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>
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>
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);
}
}
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";
Cron Integration
Run the generator after each content fetch:
# In your cron script, after fetching new videos:
php bin/generate-sitemap.php
Submitting to Search Engines
Add to your robots.txt:
Sitemap: https://dailywatch.video/sitemaps/sitemap-index.xml
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)