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.

How to Display Latest YouTube Videos on Your Astro Blog (SSG + SSR Guide)

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(/&amp;/g, "&")
        .replace(/&lt;/g, "<")
        .replace(/&gt;/g, ">")
        .replace(/&quot;/g, '"')
        .replace(/&#39;/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:

  1. Build-Time Execution: The --- section runs during build time, not in the browser
  2. RSS Feed Fetching: We use YouTube’s RSS feed (/feeds/videos.xml) which is free and doesn’t require API keys
  3. XML Parsing: We use regex to extract video data from the XML response
  4. Error Handling: Proper try-catch to handle network failures gracefully
  5. HTML Entity Decoding: Converting &amp; 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(/&amp;/g, "&")
        .replace(/&lt;/g, "<")
        .replace(/&gt;/g, ">")
        .replace(/&quot;/g, '"')
        .replace(/&#39;/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:

  1. Page loads instantly - The main page renders without waiting for YouTube data
  2. Component renders on demand - The YouTube component becomes a “server island” that renders separately
  3. No client-side JavaScript needed - Everything is handled server-side
  4. Automatic error handling - If the YouTube fetch fails, only this component is affected
  5. 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

  1. Check Channel ID: Make sure your channel ID is correct
  2. Check Network: Verify the RSS feed URL in your browser
  3. Check Console: Look for JavaScript errors in browser dev tools
  4. Check Build Logs: For SSG, check build-time errors

Performance Issues

  1. Reduce Video Count: Display fewer videos initially
  2. Implement Pagination: Load more videos on demand
  3. Optimize Images: Use smaller thumbnail sizes
  4. 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