🚀 How to Build a High-Performance YouTube Video Widget for Blogger using Cloudflare Workers 📺

0
Serverless Widget

🚀 How to Build a High-Performance, Unlimited YouTube Video Widget for Blogger using Cloudflare Workers 📺

Say goodbye to broken API quotas and exposed keys. Build a secure, self-updating YouTube feed for your Blogger site that loads instantly, caches globally, and never breaks your premium theme.

☁️ Cloudflare Worker 🎨 Premium Blogger Widget 🔒 No API Key Leaks ⚡ 99.8% Quota Reduction
Overview

🛠️ System Architecture: How It Works

Instead of exposing your YouTube API key to the browser, we introduce a secure middleman:

[Blogger Visitor] ➔ [Cloudflare Worker (Secure Proxy)] ➔ [YouTube API]
                                         └── Caches data for 1 hour

  • Security: Your private YouTube API Key is hidden safely inside Cloudflare.
  • Performance: Videos load in milliseconds because Cloudflare serves cached data globally.
  • Quota Protection: If 100,000 visitors hit your site, the YouTube API is only called 24 times (once per hour).
Prep

📋 Prerequisites

  • ✅ A Google Developer Console account with the YouTube Data API v3 enabled.
  • ✅ A free Cloudflare account.
  • ✅ Access to your Blogger dashboard (Theme > Edit HTML).
Step 1

⚡ Deploy the Secure Cloudflare Worker

Create the backend script that will fetch YouTube data without exposing your API key.

  1. Log into your Cloudflare Dashboard.
  2. Go to Workers & Pages > Create application > Create Worker.
  3. Name your worker (e.g., youtube-feed-api) and deploy.
  4. Click Edit Code and replace the default script with the following:
Cloudflare Worker (JavaScript)
export default {
  async fetch(request, env, ctx) {
    if (request.method === "OPTIONS") {
      return new Response(null, {
        headers: {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Methods": "GET, OPTIONS",
          "Access-Control-Allow-Headers": "Content-Type",
        }
      });
    }

    const API_KEY = "YOUR_SECRET_YOUTUBE_API_KEY";
    const PLAYLIST_ID = "YOUR_YOUTUBE_UPLOADS_PLAYLIST_ID";
    const MAX_RESULTS = 5;

    const url = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=${PLAYLIST_ID}&maxResults=${MAX_RESULTS}&key=${API_KEY}`;

    try {
      const response = await fetch(url, {
        headers: {
          'Accept': 'application/json',
          'Referer': 'https://www.youtube.com'
        }
      });

      if (!response.ok) {
        return new Response(JSON.stringify({ error: `YouTube API returned status ${response.status}` }), {
          status: response.status,
          headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
        });
      }

      const data = await response.json();

      return new Response(JSON.stringify(data), {
        status: 200,
        headers: {
          "Content-Type": "application/json",
          "Access-Control-Allow-Origin": "*",
          "Cache-Control": "public, max-age=3600"
        }
      });

    } catch (error) {
      return new Response(JSON.stringify({ error: error.message }), {
        status: 500,
        headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
      });
    }
  }
};
⚠️
Replace the placeholders
Set API_KEY and PLAYLIST_ID with your real values. Your uploads playlist ID usually starts with UU instead of UC.

Click Save and Deploy. Copy your newly generated Worker URL (e.g., https://your-subdomain.workers.dev).

Step 2

🎨 Inject the Smart Frontend Widget into Blogger

This widget is XML‑safe and won’t crash your premium Blogger theme. It uses a setTimeout buffer to wait for your theme’s layout scripts before rendering.

  1. Go to Blogger Dashboard > Theme > Edit HTML.
  2. Locate the <b:includable id='main'> section where you want the videos.
  3. Replace the entire content between <b:includable id='main'> and </b:includable> with the code below:
Blogger Widget (HTML + CSS + JS)
<b:includable id='main'>
  <!-- YouTube Videos Section (Premium Dark UI Layout) -->
  <style>
    .yt-section-wrapper, .yt-section-wrapper * {
      box-sizing: border-box !important;
      margin: 0; padding: 0;
    }
    .yt-section-wrapper {
      background: #0f0f0f !important;
      border-radius: 12px !important;
      padding: 20px !important;
      color: #ffffff !important;
      font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif !important;
      box-shadow: 0 4px 20px rgba(0,0,0,0.15) !important;
      margin: 20px 0 !important;
      width: 100% !important;
      display: block !important;
    }
    .yt-header {
      display: flex !important;
      justify-content: space-between !important;
      align-items: center !important;
      margin-bottom: 20px !important;
      border-bottom: 1px solid #272727 !important;
      padding-bottom: 12px !important;
      width: 100% !important;
    }
    .yt-title {
      display: flex !important;
      align-items: center !important;
      gap: 8px !important;
      font-size: 18px !important;
      font-weight: 700 !important;
      text-transform: uppercase !important;
      letter-spacing: 0.5px !important;
      color: #ffffff !important;
    }
    .yt-title::before {
      content: '' !important;
      display: inline-block !important;
      width: 4px !important;
      height: 18px !important;
      background: #ff0000 !important;
      border-radius: 2px !important;
    }
    .yt-more-btn {
      display: inline-flex !important;
      align-items: center !important;
      gap: 6px !important;
      background: #272727 !important;
      color: #f1f1f1 !important;
      text-decoration: none !important;
      font-size: 13px !important;
      font-weight: 600 !important;
      padding: 6px 14px !important;
      border-radius: 18px !important;
      transition: all 0.3s ease !important;
    }
    .yt-more-btn:hover {
      background: #ff0000 !important;
      color: #fff !important;
      transform: translateY(-2px) !important;
    }
    .yt-main-flex {
      display: flex !important;
      flex-direction: row !important;
      align-items: center !important;
      gap: 20px !important;
      width: 100% !important;
    }
    .yt-left-col { flex: 1.2 !important; width: 55% !important; }
    .yt-right-col { flex: 1 !important; width: 45% !important; }
    .yt-small-flex-grid {
      display: flex !important;
      flex-wrap: wrap !important;
      gap: 15px !important;
      width: 100% !important;
    }
    .yt-small-item { width: calc(50% - 8px) !important; min-width: 120px !important; }
    .shimmer {
      background: linear-gradient(90deg, #1f1f1f 25%, #2c2c2c 50%, #1f1f1f 75%) !important;
      background-size: 200% 100% !important;
      animation: loadingShimmer 1.5s infinite !important;
      border-radius: 8px !important;
    }
    @keyframes loadingShimmer {
      0% { background-position: 200% 0; }
      100% { background-position: -200% 0; }
    }
    .yt-card {
      position: relative !important;
      cursor: pointer !important;
      overflow: hidden !important;
      border-radius: 8px !important;
      transition: transform 0.3s ease, box-shadow 0.3s ease !important;
      background: #1f1f1f !important;
      width: 100% !important;
      display: block !important;
    }
    .yt-card:hover {
      transform: translateY(-4px) !important;
      box-shadow: 0 8px 24px rgba(255, 0, 0, 0.2) !important;
    }
    .yt-thumb-wrapper {
      position: relative !important;
      overflow: hidden !important;
      aspect-ratio: 16/9 !important;
      width: 100% !important;
    }
    .yt-thumb {
      width: 100% !important;
      height: 100% !important;
      object-fit: cover !important;
      object-position: center !important;
      display: block !important;
      transition: transform 0.5s ease !important;
    }
    .yt-card:hover .yt-thumb { transform: scale(1.05) !important; }
    .play-overlay {
      position: absolute !important;
      top: 50% !important;
      left: 50% !important;
      transform: translate(-50%, -50%) scale(0.9) !important;
      background: rgba(255, 0, 0, 0.9) !important;
      width: 44px !important;
      height: 44px !important;
      border-radius: 50% !important;
      display: flex !important;
      align-items: center !important;
      justify-content: center !important;
      opacity: 0 !important;
      transition: all 0.3s ease !important;
      box-shadow: 0 4px 15px rgba(255, 0, 0, 0.4) !important;
      z-index: 2 !important;
    }
    .yt-card:hover .play-overlay {
      opacity: 1 !important;
      transform: translate(-50%, -50%) scale(1) !important;
    }
    .play-icon {
      width: 0 !important;
      height: 0 !important;
      border-style: solid !important;
      border-width: 7px 0 7px 12px !important;
      border-color: transparent transparent transparent #ffffff !important;
      margin-left: 2px !important;
    }
    .main-info {
      position: absolute !important;
      bottom: 0 !important;
      left: 0 !important;
      right: 0 !important;
      background: linear-gradient(to top, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.7) 60%, transparent 100%) !important;
      padding: 35px 16px 16px 16px !important;
      z-index: 1 !important;
    }
    .main-title {
      margin: 0 0 6px 0 !important;
      font-size: 16px !important;
      font-weight: 700 !important;
      line-height: 1.4 !important;
      color: #ffffff !important;
      display: -webkit-box !important;
      -webkit-line-clamp: 2 !important;
      -webkit-box-orient: vertical !important;
      overflow: hidden !important;
      padding-right: 15px !important;
    }
    .small-card-content {
      padding: 8px !important;
      background: #181818 !important;
      border-bottom-left-radius: 8px !important;
      border-bottom-right-radius: 8px !important;
    }
    .small-title {
      margin: 0 0 5px 0 !important;
      font-size: 12px !important;
      font-weight: 600 !important;
      line-height: 1.4 !important;
      color: #eaeaea !important;
      display: -webkit-box !important;
      -webkit-line-clamp: 2 !important;
      -webkit-box-orient: vertical !important;
      overflow: hidden !important;
      transition: color 0.2s ease !important;
      height: 34px !important;
    }
    .yt-card:hover .small-title { color: #ff0000 !important; }
    .vid-date {
      font-size: 10px !important;
      color: #aaaaaa !important;
      display: flex !important;
      align-items: center !important;
      gap: 4px !important;
    }
    @media (max-width: 850px) {
      .yt-main-flex { flex-direction: column !important; }
      .yt-left-col { width: 100% !important; }
      .yt-right-col { width: 100% !important; }
    }
    @media (max-width: 480px) {
      .yt-small-item { width: 100% !important; }
    }
  </style>

  <div class='yt-section-wrapper'>
    <div class='yt-header'>
      <h3 class='yt-title'>Latest Videos</h3>
      <a class='yt-more-btn' href='#' id='yt-channel-link' target='_blank'>
        View Channel 
        <svg fill='none' height='12' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' viewBox='0 0 24 24' width='12'><line x1='5' x2='19' y1='12' y2='12'/><polyline points='12 5 19 12 12 19'/></svg>
      </a>
    </div>
    
    <div class='yt-main-flex'>
      <div class='yt-left-col' id='large-video-slot'>
        <div class='shimmer' style='width: 100%; height: 280px;'/>
      </div>
      <div class='yt-right-col'>
        <div class='yt-small-flex-grid' id='small-videos-slot'>
          <div class='yt-small-item shimmer' style='height: 120px;'/>
          <div class='yt-small-item shimmer' style='height: 120px;'/>
          <div class='yt-small-item shimmer' style='height: 120px;'/>
          <div class='yt-small-item shimmer' style='height: 120px;'/>
        </div>
      </div>
    </div>
  </div>

  <script type='text/javascript'>
  //<![CDATA[
  setTimeout(function() {
      var workerURL = 'https://YOUR-WORKER-SUBDOMAIN.workers.dev'; 
      var YT_CHANNEL_ID = 'YOUR_YOUTUBE_CHANNEL_ID';

      var channelBtn = document.getElementById('yt-channel-link');
      if (channelBtn) {
          channelBtn.href = 'https://www.youtube.com/channel/' + YT_CHANNEL_ID;
      }

      function printError(msg) {
          var slot = document.getElementById('large-video-slot');
          if (slot) {
              slot.innerHTML = '<div style="color:#ff4444;background:#221111;padding:15px;border-radius:8px;font-size:13px;border:1px solid #ff0000;">⚠️ Connection Status: ' + msg + '</div>';
          }
      }

      function renderVideos(videos) {
          try {
              if (!videos || videos.length === 0) {
                  printError('No videos found in YouTube response.');
                  return;
              }

              var mainVid = videos[0];
              var mainId = mainVid.snippet.resourceId.videoId; 
              var mainTitle = mainVid.snippet.title.replace(/'/g, "&apos;");
              var mainThumb = 'https://img.youtube.com/vi/' + mainId + '/maxresdefault.jpg';
              var mainDate = new Date(mainVid.snippet.publishedAt).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });

              var largeSlot = document.getElementById('large-video-slot');
              if (largeSlot) {
                  largeSlot.innerHTML = 
                      '<div class="yt-card" onclick="window.open(\'https://youtu.be/' + mainId + '\', \'_blank\')">' +
                          '<div class="yt-thumb-wrapper">' +
                              '<img class="yt-thumb" src="' + mainThumb + '" alt="' + mainTitle + '" />' +
                              '<div class="play-overlay">' +
                                  '<div class="play-icon"></div>' +
                              '</div>' +
                              '<div class="main-info">' +
                                  '<h4 class="main-title">' + mainTitle + '</h4>' +
                                  '<span class="vid-date">' + mainDate + '</span>' +
                              '</div>' +
                          '</div>' +
                      '</div>';
              }

              var smallVidsHTML = '';
              videos.forEach(function(vid, index) {
                  if (index === 0) return;
                  
                  var vidId = vid.snippet.resourceId.videoId; 
                  var vidTitle = vid.snippet.title.replace(/'/g, "&apos;");
                  var vidThumb = 'https://img.youtube.com/vi/' + vidId + '/hqdefault.jpg';
                  var vidDate = new Date(vid.snippet.publishedAt).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });

                  smallVidsHTML += 
                      '<div class="yt-small-item">' +
                          '<div class="yt-card" onclick="window.open(\'https://youtu.be/' + vidId + '\', \'_blank\')">' +
                              '<div class="yt-thumb-wrapper">' +
                                  '<img class="yt-thumb" src="' + vidThumb + '" alt="' + vidTitle + '" />' +
                                  '<div class="play-overlay" style="width: 36px; height: 36px;">' +
                                      '<div class="play-icon" style="border-width: 6px 0 6px 10px; margin-left: 2px;"></div>' +
                                  '</div>' +
                              '</div>' +
                              '<div class="small-card-content">' +
                                  '<h5 class="small-title">' + vidTitle + '</h5>' +
                                  '<span class="vid-date">' + vidDate + '</span>' +
                              '</div>' +
                          '</div>' +
                      '</div>';
              });
              
              var smallSlot = document.getElementById('small-videos-slot');
              if (smallSlot) {
                  smallSlot.innerHTML = smallVidsHTML;
              }
          } catch(e) {
              printError('Render Error: ' + e.message);
          }
      }

      var freshURL = workerURL + '?cb=' + new Date().getTime();

      fetch(freshURL)
          .then(function(res) { 
              if (!res.ok) throw new Error('HTTP Status ' + res.status);
              return res.json(); 
          })
          .then(function(data) {
              if (data && data.items) {
                  renderVideos(data.items);
              } else if (data && data.error) {
                  printError('API Error: ' + data.error.message);
              } else {
                  printError('Response missing items array.');
              }
          })
          .catch(function(err) { 
              printError(err.message); 
          });
  }, 500);
  //]]>
  </script>
</b:includable>
⚠️
Update the variables
Inside the <script>, replace workerURL with your actual Cloudflare Worker URL and YT_CHANNEL_ID with your YouTube Channel ID.

Click Save in the top right corner.

Step 3

🔍 Testing and Diagnostics

Open your live Blogger site and perform a Hard Refresh (Ctrl + F5 on Windows, Cmd + Shift + R on Mac).

💡 Built-in Error Debugger: If your configuration has an issue, the widget will display a clear, red diagnostic alert right on the page, showing you exactly what to fix.

If you see the shimmer placeholders and then the videos load, everything is working perfectly.

Features

🏆 Key Features Added to Your Blog

  • 100% Automatic Updates: New YouTube videos appear instantly on your blog without editing any code.
  • Zero Layout Shift (Shimmer Loading): Animated placeholder boxes hold the space until data loads.
  • Complete Quota Immunity: Cloudflare caches the API response, reducing Google API consumption by 99.8%.
  • Fluid Responsive UX: Cinematic hover effects, custom red play buttons, and perfect mobile breakpoints.
  • XML‑Safe for Blogger: No template crashes; the setTimeout buffer ensures compatibility with any premium theme.
Checklist

✅ Final Implementation Checklist

  • Cloudflare Worker deployed with correct API key and playlist ID
  • Worker URL copied and tested in browser (should return JSON)
  • Blogger HTML edited with the full widget code and variables updated
  • Hard refresh performed on live blog – videos load smoothly
  • Error debugger shows clear messages if something is misconfigured
  • Enjoy an always‑fresh YouTube feed with zero manual updates!

Key Takeaways

API keys stay hidden inside Cloudflare Worker
Global CDN caching eliminates quota limits
Shimmer placeholders prevent layout shifts
Premium dark UI with hover animations
XML‑safe script works in any Blogger theme
Automatically updates when you publish new videos

📺 Your YouTube Feed, Always Fresh and Secure

Now you have a professional, self‑updating video widget that doesn't break your blog or leak your API key. Try it today and let your latest content shine. If you run into any issues, drop a comment below!

Post a Comment

0 Comments

Join the tech debate...
We love a good discussion, but please keep it respectful and relevant to the topic. Vulgarity, personal attacks, and spam will be removed. Let’s keep the community smart, helpful, and welcoming to all tech fans!

Join the tech debate...
We love a good discussion, but please keep it respectful and relevant to the topic. Vulgarity, personal attacks, and spam will be removed. Let’s keep the community smart, helpful, and welcoming to all tech fans!

Post a Comment (0)
To Top