How I Added a Related Posts Section in Blogger (With Randomized Results)


Here's how I set up a "Related Posts" section on my Blogger post pages. The goal: after each post, show up to 4 other posts that share at least one label (category) with the current one. The results are randomized within each group of label matches, so the related posts don't look the same every time.

Important Blogger Settings

  • Enable your blog feed: Go to Settings > Site feed and set Allow blog feed to Full (or at least Until Jump Break). If the feed is off or set to "None," the script can't fetch related posts.
  • Labels must be visible: Make sure your post template includes the labels block, so the script can read the current post's labels.

1. The HTML Block

I placed this right after the post content in my theme. It only appears on single post pages.

<b:if cond='data:blog.pageType == "item"'>
  <div class='related-posts-container'>
    <h3 class='related-posts-title'>Related Posts</h3>
    <div class='related-posts-grid' id='related-posts-grid'>
      <!-- Posts will be loaded here -->
    </div>
  </div>
</b:if>

2. The JavaScript (Randomized Related Posts)

This is the script I used. It grabs up to 2 labels from the current post, fetches posts with those labels, and displays up to 4 results (excluding the current post). The results are sorted by best label match, but randomized within each match group. I put this just before </body> in the theme.

<script>
//<![CDATA[
(function() {
  // Only run on individual post pages
  if (!/\/\d{4}\/\d{2}\//.test(window.location.pathname)) return;

  var maxPosts = 4; // Change as needed
  var container = document.getElementById('related-posts-grid');
  if (!container) return;

  // Get up to 2 labels from the current post
  var labelLinks = document.querySelectorAll('.labels a[rel="tag"]');
  var labels = [];
  labelLinks.forEach(function(link) {
    var label = link.textContent.trim();
    if (label && labels.indexOf(label) === -1) labels.push(label);
  });
  var labelsToUse = labels.slice(0, 2);
  if (!labelsToUse.length) return;

  // Fetch posts for each label (2 fetches only)
  var fetches = labelsToUse.map(function(label) {
    var url = '/feeds/posts/default/-/' + encodeURIComponent(label) + '?alt=json&max-results=8';
    return fetch(url)
      .then(function(res) { return res.json(); })
      .then(function(data) { return data.feed && data.feed.entry ? data.feed.entry : []; });
  });

  Promise.all(fetches).then(function(results) {
    var postsMap = {};
    var currentUrl = window.location.href.replace(/#.*$/, '');

    // Flatten and score posts
    results.flat().forEach(function(entry) {
      var postUrl = '';
      for (var i = 0; i < entry.link.length; i++) {
        if (entry.link[i].rel === 'alternate') {
          postUrl = entry.link[i].href;
          break;
        }
      }
      if (postUrl === currentUrl) return;

      // Get post labels
      var postLabels = (entry.category || []).map(function(cat) { return cat.term; });
      // Score: number of matching labels (max 2)
      var score = labelsToUse.filter(function(l) { return postLabels.includes(l); }).length;

      // Use highest score if duplicate
      if (!postsMap[postUrl] || postsMap[postUrl].score < score) {
        postsMap[postUrl] = {
          entry: entry,
          score: score,
          published: entry.published.$t
        };
      }
    });

    // Convert map to array, sort by score then randomize within same score
    var sorted = Object.values(postsMap)
      .sort(function(a, b) {
        if (b.score !== a.score) return b.score - a.score; // Score first
        return 0.5 - Math.random(); // Random within same score
      })
      .slice(0, maxPosts);

    // Render
    var html = '';
    sorted.forEach(function(item) {
      var entry = item.entry;
      var postUrl = entry.link.find(function(l) { return l.rel === 'alternate'; }).href;
      var title = entry.title.$t || 'Untitled';
      var snippet = '';
      if (entry.summary && entry.summary.$t) {
        snippet = entry.summary.$t.replace(/<[^>]+>/g, '');
      } else if (entry.content && entry.content.$t) {
        snippet = entry.content.$t.replace(/<[^>]+>/g, '').substring(0, 150) + '...';
      }
      
      // Get date
      var date = new Date(entry.published.$t).toLocaleDateString('en-US', {
        year: 'numeric',
        month: 'short',
        day: 'numeric'
      });
      
      html += '<div class="related-post-card">';
      html += '  <div class="related-post-content">';
      html += '    <a href="' + postUrl + '" class="related-post-title">' + title + '</a>';
      html += '    <div class="related-post-snippet">' + snippet + '</div>';
      html += '    <div class="related-post-meta">';
      html += '      <div class="related-post-date">';
      html += '        <i class="fa fa-calendar" aria-hidden="true"></i>';
      html += '        <span>' + date + '</span>';
      html += '      </div>';
      html += '    </div>';
      html += '  </div>';
      html += '</div>';
    });
    container.innerHTML = html;
  }).catch(function() {
    document.querySelector('.related-posts-container').style.display = 'none';
  });
})();
//]]>
</script>

3. The CSS

This is the CSS I used for a clean, responsive look. Add it to your theme’s <b:skin><![CDATA[ ... ]]></b:skin> section.

.related-posts-container {
  margin: 2.5rem 0;
  padding: 0 1.25rem;
}
.related-posts-title {
  font-size: 1.5rem;
  font-weight: 600;
  margin-bottom: 1.5rem;
  color: #2a2a2a;
  text-align: left;
}
.related-posts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}
.related-post-card {
  background: #fff;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0,0,0,0.05);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  border: 1px solid #f0f0f0;
}
.related-post-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.related-post-content {
  padding: 1.25rem;
}
.related-post-title {
  font-size: 1.1rem;
  font-weight: 600;
  line-height: 1.4;
  margin-bottom: 0.75rem;
  color: #2a2a2a;
  text-decoration: none;
  display: block;
}
.related-post-title:hover {
  color: #D2691E;
}
.related-post-snippet {
  font-size: 0.95rem;
  line-height: 1.6;
  color: #666;
  margin-bottom: 0.75rem;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.related-post-meta {
  font-size: 0.85rem;
  color: #888;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.related-post-date {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}
@media (max-width: 768px) {
  .related-posts-grid {
    grid-template-columns: 1fr;
    gap: 1rem;
  }
  .related-posts-container {
    padding: 0 1rem;
  }
  .related-post-content {
    padding: 1rem;
  }
}

How It Works

  • Runs only on single post pages.
  • Finds up to 2 labels from the current post.
  • Fetches posts with those labels using Blogger’s JSON feed.
  • Shows up to 4 related posts (excluding the current post), sorted by best label match and randomized within each group.
  • Displays each related post’s title, snippet, and date in a card layout.
  • Hides the section if nothing is found or there’s an error.