DEV Community

ahmet gedik
ahmet gedik

Posted on

Building a Video Sitemap Generator from Scratch

Google's standard XML sitemap tells search engines about your pages. A video sitemap goes further, providing rich metadata that helps your video content appear in Google Video search results and earn rich snippets.

In this article, I'll show you how I built a video sitemap generator in PHP for DailyWatch, a platform with thousands of video pages that need to be discoverable.

Why Video Sitemaps?

A standard sitemap entry looks like this:

<url>
  <loc>https://dailywatch.video/watch/abc123</loc>
  <lastmod>2026-02-15</lastmod>
</url>
Enter fullscreen mode Exit fullscreen mode

A video sitemap entry provides much richer data:

<url>
  <loc>https://dailywatch.video/watch/abc123</loc>
  <video:video>
    <video:thumbnail_loc>https://i.ytimg.com/vi/abc123/mqdefault.jpg</video:thumbnail_loc>
    <video:title>Amazing Music Video 2026</video:title>
    <video:description>Official music video for...</video:description>
    <video:player_loc>https://www.youtube.com/embed/abc123</video:player_loc>
    <video:duration>272</video:duration>
    <video:view_count>1500000</video:view_count>
    <video:publication_date>2026-02-10T08:00:00Z</video:publication_date>
  </video:video>
</url>
Enter fullscreen mode Exit fullscreen mode

This metadata helps Google understand and rank your video content.

The Generator Class

class VideoSitemapGenerator {
    private const MAX_URLS_PER_SITEMAP = 50000;
    private const XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>';

    public function __construct(
        private readonly PDO $db,
        private readonly string $baseUrl,
        private readonly string $outputPath,
    ) {
        if (!is_dir($this->outputPath)) {
            mkdir($this->outputPath, 0755, true);
        }
    }

    public function generate(): array {
        $videos = $this->fetchAllVideos();
        $chunks = array_chunk($videos, self::MAX_URLS_PER_SITEMAP);
        $sitemapFiles = [];

        foreach ($chunks as $index => $chunk) {
            $filename = 'video-sitemap-' . ($index + 1) . '.xml';
            $this->writeSitemap($chunk, $filename);
            $sitemapFiles[] = $filename;
        }

        $this->writeSitemapIndex($sitemapFiles);

        return $sitemapFiles;
    }

    private function fetchAllVideos(): array {
        $stmt = $this->db->query("
            SELECT video_id, title, description, channel_title,
                   thumbnail_url, duration, view_count,
                   published_at, fetched_at
            FROM videos
            WHERE title IS NOT NULL AND title != ''
            ORDER BY fetched_at DESC
        ");

        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function writeSitemap(array $videos, string $filename): void {
        $xml = self::XML_HEADER . "\n";
        $xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' . "\n";
        $xml .= '        xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">' . "\n";

        foreach ($videos as $video) {
            $xml .= $this->buildVideoEntry($video);
        }

        $xml .= "</urlset>\n";

        file_put_contents($this->outputPath . '/' . $filename, $xml);
    }

    private function buildVideoEntry(array $video): string {
        $loc = htmlspecialchars($this->baseUrl . '/watch/' . $video['video_id']);
        $title = htmlspecialchars($video['title'], ENT_XML1);
        $desc = htmlspecialchars(
            mb_substr($video['description'] ?? $video['title'], 0, 2048),
            ENT_XML1
        );
        $thumb = htmlspecialchars($video['thumbnail_url']);
        $player = htmlspecialchars(
            'https://www.youtube.com/embed/' . $video['video_id']
        );
        $duration = $this->parseDurationToSeconds($video['duration'] ?? '');
        $pubDate = $video['published_at'] ?? $video['fetched_at'];

        $entry = "  <url>\n";
        $entry .= "    <loc>{$loc}</loc>\n";
        $entry .= "    <video:video>\n";
        $entry .= "      <video:thumbnail_loc>{$thumb}</video:thumbnail_loc>\n";
        $entry .= "      <video:title>{$title}</video:title>\n";
        $entry .= "      <video:description>{$desc}</video:description>\n";
        $entry .= "      <video:player_loc>{$player}</video:player_loc>\n";

        if ($duration > 0) {
            $entry .= "      <video:duration>{$duration}</video:duration>\n";
        }
        if (!empty($video['view_count'])) {
            $entry .= "      <video:view_count>{$video['view_count']}</video:view_count>\n";
        }
        if ($pubDate) {
            $entry .= "      <video:publication_date>{$pubDate}</video:publication_date>\n";
        }

        $entry .= "    </video:video>\n";
        $entry .= "  </url>\n";

        return $entry;
    }

    private function parseDurationToSeconds(string $isoDuration): int {
        if (empty($isoDuration)) return 0;

        // Parse ISO 8601 duration (PT4M32S)
        if (preg_match('/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/', $isoDuration, $m)) {
            $hours = (int)($m[1] ?? 0);
            $minutes = (int)($m[2] ?? 0);
            $seconds = (int)($m[3] ?? 0);
            return $hours * 3600 + $minutes * 60 + $seconds;
        }

        return 0;
    }

    private function writeSitemapIndex(array $sitemapFiles): void {
        $xml = self::XML_HEADER . "\n";
        $xml .= '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";

        foreach ($sitemapFiles as $file) {
            $loc = htmlspecialchars($this->baseUrl . '/sitemaps/' . $file);
            $lastmod = date('Y-m-d\TH:i:sP');
            $xml .= "  <sitemap>\n";
            $xml .= "    <loc>{$loc}</loc>\n";
            $xml .= "    <lastmod>{$lastmod}</lastmod>\n";
            $xml .= "  </sitemap>\n";
        }

        $xml .= "</sitemapindex>\n";

        file_put_contents($this->outputPath . '/sitemap-index.xml', $xml);
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage

$db = new PDO('sqlite:data/videos.db');
$generator = new VideoSitemapGenerator(
    db: $db,
    baseUrl: 'https://dailywatch.video',
    outputPath: 'public/sitemaps'
);

$files = $generator->generate();
echo "Generated " . count($files) . " sitemap files\n";
Enter fullscreen mode Exit fullscreen mode

Cron Integration

Run the generator after each content fetch:

# In your cron script, after fetching new videos:
php bin/generate-sitemap.php
Enter fullscreen mode Exit fullscreen mode

Submitting to Search Engines

Add to your robots.txt:

Sitemap: https://dailywatch.video/sitemaps/sitemap-index.xml
Enter fullscreen mode Exit fullscreen mode

And submit directly to Google Search Console and Bing Webmaster Tools for faster discovery.

This approach powers the sitemap generation at dailywatch.video, helping thousands of video pages get indexed by search engines.

Top comments (0)