How to Display Latest YouTube Videos on Your Astro Blog (SSG + SSR Guide)
Learn how to automatically display your latest YouTube videos on your Astro blog using both Static Site Generation (SSG) and Server-Side Rendering (SSR) approaches. Complete with code examples and explanations for beginners.

Table of Contents
- 🎯 What You’ll Learn
- 🛠️ Prerequisites
- 🏗️ Method 1: SSG Approach (Build-Time Fetching)
- 🚀 Method 2: SSR Approach with server:defer
- 🌟 Enhanced Version for Cloudflare Pages
- 🎯 When to Use Each Approach
- đź”§ Customization Options
- 🚀 Performance Optimization Tips
- 🔍 Troubleshooting Common Issues
- 🎉 Conclusion
Ever wanted to showcase your latest YouTube videos directly on your blog without manually updating them every time you publish new content? Well, you’re in luck! Today we’re diving into how to automatically display your latest YouTube videos on your Astro blog. 🎥✨
Whether you’re using the free Astro blog theme we covered earlier or your own custom setup, this guide will walk you through two powerful approaches: Static Site Generation (SSG) for blazing-fast performance, and Server-Side Rendering (SSR) for real-time updates.
🎯 What You’ll Learn
-
SSG Approach: Fetch YouTube videos at build time for maximum performance
-
SSR Approach: Dynamic video fetching with server-side rendering
-
RSS Feed Parsing: How to work with YouTube’s RSS feeds
-
Error Handling: Making your component bulletproof
-
Performance Optimization: Best practices for both approaches
🛠️ Prerequisites
Before we start coding, make sure you have:
- An Astro blog (check out our free Astro blog guide if you need one!)
- Basic knowledge of JavaScript/TypeScript
- A YouTube channel ID (we’ll show you how to find it)
- Node.js installed on your machine
Channel ID vs Username
YouTube Channel IDs look like this: UCGsUtKhXsRrMvYAWm8q0bCg
. You can find yours by going to your YouTube channel and looking at the URL, or using tools like YouTube Channel ID finder.
🏗️ Method 1: SSG Approach (Build-Time Fetching)
The SSG approach fetches your YouTube videos during the build process, creating static HTML that loads instantly. This is perfect for most blogs where you don’t need real-time updates.
Step 1: Create the SSG Component
Let’s create a component that fetches YouTube videos at build time:
---
// src/components/YouTubeVideosSSG.astro
import { Icon } from "astro-icon/components";
import { Image } from "astro:assets";
// Your YouTube Channel ID
const CHANNEL_ID = "UCGsUtKhXsRrMvYAWm8q0bCg"; // Replace with your channel ID
const MAX_VIDEOS = 6; // Number of videos to display
// Fetch YouTube RSS feed at build time
let videos = [];
let error = null;
try {
const response = await fetch(
`https://www.youtube.com/feeds/videos.xml?channel_id=${CHANNEL_ID}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const xmlText = await response.text();
// Parse XML to extract video data
const videoRegex =
/<entry>.*?<yt:videoId>(.*?)<\/yt:videoId>.*?<title>(.*?)<\/title>.*?<published>(.*?)<\/published>.*?<\/entry>/gs;
let match;
while ((match = videoRegex.exec(xmlText)) !== null && videos.length < MAX_VIDEOS) {
const [, videoId, title, published] = match;
videos.push({
id: videoId,
title: title
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'"),
published: new Date(published),
thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
url: `https://www.youtube.com/watch?v=${videoId}`,
});
}
} catch (err) {
console.error("Error fetching YouTube videos:", err);
error = err.message;
}
---
<section class="py-8 bg-white dark:bg-gray-900 rounded-lg">
<div class="max-w-5xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8">
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
Latest YouTube Videos
</h2>
<p class="text-lg text-gray-600 dark:text-gray-300">
Check out our latest content on YouTube
</p>
</div>
{error ? (
<div class="text-center py-8">
<p class="text-red-600 dark:text-red-400">
Unable to load YouTube videos. Please try again later.
</p>
</div>
) : videos.length === 0 ? (
<div class="text-center py-8">
<p class="text-gray-600 dark:text-gray-400">
No videos found.
</p>
</div>
) : (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{videos.map((video) => (
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1 border border-gray-200 dark:border-gray-700">
<div class="relative">
<a href={video.url} target="_blank" rel="noopener noreferrer">
<Image
src={video.thumbnail}
alt={video.title}
width={320}
height={180}
class="w-full h-48 object-cover rounded-t-lg"
loading="lazy"
format="webp"
/>
</a>
<a href={video.url} target="_blank" rel="noopener noreferrer">
<Icon
name="mdi:play-circle"
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white w-16 h-16 drop-shadow-lg opacity-80 hover:opacity-100 transition-opacity"
/>
</a>
</div>
<div class="p-4">
<a href={video.url} target="_blank" rel="noopener noreferrer">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white line-clamp-2 mb-2 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
{video.title}
</h3>
</a>
<p class="text-sm text-gray-500 dark:text-gray-400">
{video.published.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
})}
</p>
</div>
</div>
))}
</div>
)}
<div class="text-center mt-8">
<a
href={`https://www.youtube.com/channel/${CHANNEL_ID}`}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center px-6 py-3 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors duration-300"
>
<Icon name="mdi:youtube" class="w-5 h-5 mr-2" />
View All Videos
</a>
</div>
</div>
</section>
<style>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
Understanding the SSG Code
Let’s break down what’s happening here:
- Build-Time Execution: The
---
section runs during build time, not in the browser - RSS Feed Fetching: We use YouTube’s RSS feed (
/feeds/videos.xml
) which is free and doesn’t require API keys - XML Parsing: We use regex to extract video data from the XML response
- Error Handling: Proper try-catch to handle network failures gracefully
- HTML Entity Decoding: Converting
&
back to&
, etc.
Why RSS Over API?
YouTube’s RSS feed is perfect for this use case because it’s free, doesn’t require authentication, and provides all the data we need without rate limits!
Step 2: Using the SSG Component
Add the component to any page or layout:
---
// src/pages/index.astro (or wherever you want it)
import YouTubeVideosSSG from '../components/YouTubeVideosSSG.astro';
---
<html>
<body>
<!-- Your other content -->
<YouTubeVideosSSG />
</body>
</html>
🚀 Method 2: SSR Approach with server:defer
The SSR approach with server:defer
gives you the best of both worlds: real-time updates with excellent performance. The server:defer
directive transforms the component into a server island that renders on demand.
Step 1: Configure Astro for SSR
First, install the Node.js adapter :
npx astro add node
This would make the astro.config.mjs
to look like below:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // Enable SSR
adapter: node({
mode: 'standalone'
}),
});
Step 2: Create the SSR Component with server:defer
---
// src/components/YouTubeVideosSSR.astro
import { Icon } from "astro-icon/components";
import { Image } from "astro:assets";
export interface Props {
channelId: string;
maxVideos?: number;
}
const { channelId, maxVideos = 6 } = Astro.props;
// Fetch YouTube videos - this runs on the server when the island is rendered
let videos = [];
let error = null;
try {
const response = await fetch(
`https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const xmlText = await response.text();
// Parse XML to extract video data
const videoRegex =
/<entry>.*?<yt:videoId>(.*?)<\/yt:videoId>.*?<title>(.*?)<\/title>.*?<published>(.*?)<\/published>.*?<\/entry>/gs;
let match;
while ((match = videoRegex.exec(xmlText)) !== null && videos.length < maxVideos) {
const [, videoId, title, published] = match;
videos.push({
id: videoId,
title: title
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'"),
published: new Date(published),
thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
url: `https://www.youtube.com/watch?v=${videoId}`,
});
}
} catch (err) {
console.error("Error fetching YouTube videos:", err);
error = err.message;
}
---
<section class="py-8 bg-white dark:bg-gray-900 rounded-lg">
<div class="max-w-5xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8">
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
Latest YouTube Videos
</h2>
<p class="text-lg text-gray-600 dark:text-gray-300">
Check out our latest content on YouTube
</p>
</div>
{error ? (
<div class="text-center py-8">
<p class="text-red-600 dark:text-red-400">
Unable to load YouTube videos. Please try again later.
</p>
</div>
) : videos.length === 0 ? (
<div class="text-center py-8">
<p class="text-gray-600 dark:text-gray-400">
No videos found.
</p>
</div>
) : (
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{videos.map((video) => (
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1 border border-gray-200 dark:border-gray-700">
<div class="relative">
<a href={video.url} target="_blank" rel="noopener noreferrer">
<Image
src={video.thumbnail}
alt={video.title}
width={320}
height={180}
class="w-full h-48 object-cover rounded-t-lg"
loading="lazy"
format="webp"
/>
</a>
<a href={video.url} target="_blank" rel="noopener noreferrer">
<Icon
name="mdi:play-circle"
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white w-16 h-16 drop-shadow-lg opacity-80 hover:opacity-100 transition-opacity"
/>
</a>
</div>
<div class="p-4">
<a href={video.url} target="_blank" rel="noopener noreferrer">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white line-clamp-2 mb-2 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
{video.title}
</h3>
</a>
<p class="text-sm text-gray-500 dark:text-gray-400">
{video.published.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
})}
</p>
</div>
</div>
))}
</div>
)}
<div class="text-center mt-8">
<a
href={`https://www.youtube.com/channel/${channelId}`}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center px-6 py-3 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors duration-300"
>
<Icon name="mdi:youtube" class="w-5 h-5 mr-2" />
View All Videos
</a>
</div>
</div>
</section>
<style>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
Step 3: Using the SSR Component with server:defer
---
// src/pages/index.astro
import YouTubeVideosSSR from '../components/YouTubeVideosSSR.astro';
---
<html>
<body>
<!-- Your other content -->
<!-- The server:defer directive makes this a server island -->
<YouTubeVideosSSR
server:defer
channelId="UCGsUtKhXsRrMvYAWm8q0bCg"
maxVideos={6}
/>
</body>
</html>
Understanding server:defer
The server:defer
directive is the magic that makes this approach so powerful:
- Page loads instantly - The main page renders without waiting for YouTube data
- Component renders on demand - The YouTube component becomes a “server island” that renders separately
- No client-side JavaScript needed - Everything is handled server-side
- Automatic error handling - If the YouTube fetch fails, only this component is affected
- Better user experience - Users see the page immediately, videos load progressively
Why server:defer is Amazing
With server:defer
, you get the performance benefits of static generation for your main content, while having dynamic, fresh data for specific components. It’s the perfect hybrid approach!
🌟 Enhanced Version for Cloudflare Pages
If you’re using Cloudflare Pages (like in our free Astro blog guide), you’ll need to use the Cloudflare adapter:
npx astro add cloudflare
🎯 When to Use Each Approach
Use SSG When:
-
Performance is critical - SSG sites load instantly
-
You don’t need real-time updates - Videos update only when you rebuild
-
You have limited server resources - No server-side processing needed
-
SEO is important - Static content is easily crawlable
-
You want lower costs - No server runtime costs
Use SSR When:
-
Real-time updates are important - Videos appear immediately after publishing
-
You have dynamic content needs - Different videos for different users
-
You want server-side caching - Better control over cache strategies
-
You need user-specific content - Personalized video recommendations
-
You have server infrastructure - Can handle server-side processing
đź”§ Customization Options
Styling the Components
Both components use Tailwind CSS classes. You can customize the appearance by modifying the classes:
<!-- Change the grid layout -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Change the card styling -->
<div class="bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl shadow-xl">
<!-- Change the hover effects -->
<div class="transform hover:scale-105 transition-transform duration-300">
Adding More Video Data
You can extract additional data from the RSS feed:
// Add description extraction
const descriptionRegex = /<media:description>(.*?)<\/media:description>/s;
const descMatch = descriptionRegex.exec(entry);
const description = descMatch ? descMatch[1] : '';
// Add view count (requires YouTube API)
// Add duration (requires YouTube API)
Error Handling Improvements
Add retry logic and better error states:
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (response.ok) return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
🚀 Performance Optimization Tips
1. Image Optimization
Use Astro’s Image component for automatic optimization:
<Image
src={video.thumbnail}
alt={video.title}
width={320}
height={180}
format="webp"
quality={80}
loading="lazy"
/>
2. Caching Strategies
For SSR, implement proper caching:
// Cache in memory for development
const cache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
function getCachedVideos(channelId) {
const cacheKey = `videos_${channelId}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.data;
}
return null;
}
3. Lazy Loading
Implement intersection observer for better performance:
// Only load videos when component is visible
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadYouTubeVideos();
observer.disconnect();
}
});
});
observer.observe(document.getElementById('youtube-videos-container'));
🔍 Troubleshooting Common Issues
RSS Feed Not Loading
CORS Issues
If you’re getting CORS errors in development, YouTube’s RSS feed should work fine in production. For local development, you might need to use a CORS proxy or test the production build.
Videos Not Displaying
- Check Channel ID: Make sure your channel ID is correct
- Check Network: Verify the RSS feed URL in your browser
- Check Console: Look for JavaScript errors in browser dev tools
- Check Build Logs: For SSG, check build-time errors
Performance Issues
- Reduce Video Count: Display fewer videos initially
- Implement Pagination: Load more videos on demand
- Optimize Images: Use smaller thumbnail sizes
- Add Loading States: Improve perceived performance
🎉 Conclusion
You now have two powerful ways to display YouTube videos on your Astro blog! The SSG approach gives you blazing-fast performance with build-time generation, while the SSR approach provides real-time updates with server-side rendering.
Choose the method that best fits your needs:
- Go with SSG if you want maximum performance and don’t mind rebuilding to show new videos
- Choose SSR if you need real-time updates and have server infrastructure
Both approaches work perfectly with the free Astro blog theme we covered earlier, so you can enhance your blog with dynamic YouTube content right away!
Pro Tip
You can even combine both approaches - use SSG for your main video showcase and SSR for a “Latest Video” widget that updates in real-time!
Happy coding, and may your YouTube videos get all the views they deserve! 🎬✨
Want to take your Astro blog even further? Check out our other guides on building a free Astro blog and advanced Astro techniques!
Related Posts

How To Add A Contact Form To Any Static Website
Add a contact for to any static website free and easy in 2024 with formsubmit.co.

How to Deploy Astro on Your VPS with EasyPanel
Learn how to deploy Astro static website on your own VPS with EasyPanel

Deploy Your Astro + Convex App to Vercel: The Simplest Production Setup
Deploy your real-time Astro and Convex application to Vercel in minutes with zero configuration - the easiest way to go from development to production