Category that loads random topics?

I tried to Google it, but couldn’t find anything related to this.
I think that some topics can be very valuable, but users tend to focus more on the latest ones (unless they come to a forum from a Google search, for example).

Is it possible, maybe through a component/plugin, to have a category or something like that, where it loads random topics automatically? This would revive some old posts that maybe 3 years ago didn’t have that much engagement, but were still valuable, and maybe now with more users it could have the attention they deserve.

To be clear, I don’t want the random topics to be limited to a single category, even though that could also be good. I was thinking more of a global feature where any topic from any category could be loaded into that section. basically the same way that when I go to the home page of https://meta.discourse.org/ I can see a list of all the latest topics, regardless of the category, but in my case, it would load random topics also from any category, but not focused on the topics’ date or even latest replies.

Hope it makes sense…

2 Likes

I like the idea of random topics, but I suppose there should be tweaks to be made.
My forum is 20+ years old and it would be weird to have a page with a bunch of random topics older than a decade.

If you want to target topics with low engagement, there should be a parameter like the max number of replies to show up.

Even so, it doesn’t mean that all these low-engagement topics are interesting.

Do you want to target topics on some criteria, or just… Random ones?

The closest thing that exists lies in the category settings:

It was (maybe still is?) used here on Support, as a Support topics’ ideal state is solved/closed. Open topics here typically means that no solution was provided, so it gives a chance to low-engagement topics to be looked at, even if it is months of years after - it can always be valuable.

1 Like

Random ones.

It could be a specific category, for example, or even a button to Randomize.
Or maybe just a section at the bottom of the latest posts that would load like 3 or 4 old random posts?

Even posts from your 20+ years old forum can be relevant today, but maybe when they were created, no one really paid attention and they could get some “love” now :wink:

2 Likes

Today I decided to ask Claude to help me with this.
This is what we came up with, after lots of testing and fixing (it only loads OPEN topics, which makes more sense to me):

When clicking the “Load more random topics” it loads 5 more topics below the current ones. Clicking the button again, adds 5 more, until there’s no more topics:

To load the random topics, just visit:
yourwebsite.com/?random


Here’s how to implement it:

1 - Create a component
2 - Add this to the JS tab:

import { apiInitializer } from "discourse/lib/api";
// Hide content immediately if ?random is in URL
if (new URLSearchParams(window.location.search).has('random')) {
  const style = document.createElement('style');
  style.textContent = '#main-outlet { display: none; }';
  document.head.appendChild(style);
}

let loadedTopicIds = new Set();

function addRandomTopics(listDiv, button) {
  // Hide button and show loading
  button.style.display = 'none';
  const loadingDiv = document.createElement('div');
  loadingDiv.className = 'loading-more';
  loadingDiv.textContent = 'Loading...';
  listDiv.appendChild(loadingDiv);
  
  const query = `status:open`;
  
  fetch(`/search.json?q=${encodeURIComponent(query)}`)
    .then((response) => response.json())
    .then((data) => {
      loadingDiv.remove();
      
      if (!data || !data.topics || data.topics.length === 0) {
        button.remove();
        const noMoreDiv = document.createElement('div');
        noMoreDiv.className = 'no-more-topics';
        noMoreDiv.textContent = 'No more topics to load';
        listDiv.appendChild(noMoreDiv);
        return;
      }
      
      // Filter out already loaded topics
      const unloadedTopics = data.topics.filter(topic => !loadedTopicIds.has(topic.id));
      
      if (unloadedTopics.length === 0) {
        button.remove();
        const noMoreDiv = document.createElement('div');
        noMoreDiv.className = 'no-more-topics';
        noMoreDiv.textContent = 'No more topics to load';
        listDiv.appendChild(noMoreDiv);
        return;
      }
      
      // Shuffle and pick up to 5 random topics
      const shuffled = unloadedTopics.sort(() => 0.5 - Math.random());
      const selected = shuffled.slice(0, Math.min(5, unloadedTopics.length));
      
      // Get category from Discourse site data
      const site = Discourse.__container__.lookup('site:main');
      
      selected.forEach(topic => {
        loadedTopicIds.add(topic.id);
        
        const date = new Date(topic.created_at);
        const formattedDate = date.toLocaleDateString('en-US', { 
          year: 'numeric', 
          month: 'short', 
          day: 'numeric' 
        });
        
        const itemDiv = document.createElement('div');
        itemDiv.className = 'random-topic-item';
        
        const linkDiv = document.createElement('div');
        linkDiv.className = 'random-topic-link';
        
        const link = document.createElement('a');
        link.href = `/t/${topic.slug}/${topic.id}`;
        link.textContent = topic.unicode_title || topic.title;
        
        linkDiv.appendChild(link);
        
        const metaDiv = document.createElement('div');
        metaDiv.className = 'random-topic-meta';
        
        const category = site.categories.find(cat => cat.id === topic.category_id);
        const categoryName = category?.name || 'Uncategorized';
        
        metaDiv.textContent = `${categoryName} • ${formattedDate}`;
        
        itemDiv.appendChild(linkDiv);
        itemDiv.appendChild(metaDiv);
        
        // Insert before the button
        listDiv.insertBefore(itemDiv, button);
      });
      
      // Check if we've loaded all available topics
      if (selected.length < 5 || loadedTopicIds.size >= data.topics.length) {
        button.remove();
        const noMoreDiv = document.createElement('div');
        noMoreDiv.className = 'no-more-topics';
        noMoreDiv.textContent = 'No more topics to load';
        listDiv.appendChild(noMoreDiv);
      } else {
        button.style.display = 'block';
      }
    })
    .catch((err) => {
      console.error("Fetch error:", err);
      loadingDiv.remove();
      button.style.display = 'block';
    });
}

function loadRandomTopics(container) {
  // Reset loaded topics
  loadedTopicIds = new Set();
  
  container.innerHTML = '<div class="spinner"></div>';
  
  const query = `status:open`;
  
  fetch(`/search.json?q=${encodeURIComponent(query)}`)
    .then((response) => response.json())
    .then((data) => {
      if (!data || !data.topics || data.topics.length === 0) {
        container.innerHTML = '<p>No topics found</p>';
        return;
      }
      
      // Shuffle and pick 5 random topics
      const shuffled = data.topics.sort(() => 0.5 - Math.random());
      const selected = shuffled.slice(0, 5);
      
      // Build the HTML
      const listDiv = document.createElement('div');
      listDiv.className = 'random-topics-list';
      
      const heading = document.createElement('h2');
      heading.textContent = 'Random Open Topics';
      listDiv.appendChild(heading);
      
      // Get category from Discourse site data
      const site = Discourse.__container__.lookup('site:main');
      
      selected.forEach(topic => {
        loadedTopicIds.add(topic.id);
        
        const date = new Date(topic.created_at);
        const formattedDate = date.toLocaleDateString('en-US', { 
          year: 'numeric', 
          month: 'short', 
          day: 'numeric' 
        });
        
        const itemDiv = document.createElement('div');
        itemDiv.className = 'random-topic-item';
        
        const linkDiv = document.createElement('div');
        linkDiv.className = 'random-topic-link';
        
        const link = document.createElement('a');
        link.href = `/t/${topic.slug}/${topic.id}`;
        link.textContent = topic.unicode_title || topic.title;
        
        linkDiv.appendChild(link);
        
        const metaDiv = document.createElement('div');
        metaDiv.className = 'random-topic-meta';
        
        const category = site.categories.find(cat => cat.id === topic.category_id);
        const categoryName = category?.name || 'Uncategorized';
        
        metaDiv.textContent = `${categoryName} • ${formattedDate}`;
        
        itemDiv.appendChild(linkDiv);
        itemDiv.appendChild(metaDiv);
        listDiv.appendChild(itemDiv);
      });
      
      const button = document.createElement('button');
      button.className = 'btn btn-primary load-more-random';
      button.textContent = 'Load more random topics';
      button.addEventListener('click', () => {
        addRandomTopics(listDiv, button);
      });
      
      listDiv.appendChild(button);
      container.innerHTML = '';
      container.appendChild(listDiv);
    })
    .catch((err) => {
      console.error("Fetch error:", err);
      container.innerHTML = '<p>Error loading topics</p>';
    });
}

export default apiInitializer("1.8.0", (api) => {
  let wasOnRandomPage = new URLSearchParams(window.location.search).has('random');
  
  api.onPageChange(() => {
    const urlParams = new URLSearchParams(window.location.search);
    const isOnRandomPage = urlParams.has('random');
    
    // If we were on random page but now we're not, force reload
    if (wasOnRandomPage && !isOnRandomPage) {
      window.location.reload();
      return;
    }
    
    wasOnRandomPage = isOnRandomPage;
    
    if (!isOnRandomPage) {
      return;
    }
    
    // Show and replace the content
    const mainContent = document.querySelector('#main-outlet');
    if (mainContent) {
      mainContent.style.display = 'block';
      mainContent.innerHTML = '<div id="random-topics-container"></div>';
      
      const container = document.querySelector('#random-topics-container');
      loadRandomTopics(container);
    }
  });
});

3 - Add this to the CSS tab:

.random-topics-list {
  max-width: 800px;
  margin: 40px auto;
  padding: 20px;
}

.random-topics-list h2 {
  margin-bottom: 30px;
  font-size: 2em;
}

.random-topic-item {
  margin-bottom: 25px;
}

.random-topic-link a {
  color: #E9E9E9;
  text-decoration: underline;
  font-size: 1.1em;
}

.random-topic-link a:hover {
  color: #229ED7;
}

.random-topic-meta {
  color: #808080;
  font-size: 0.85em;
  margin-top: 4px;
}

.load-more-random {
  margin-top: 30px;
}

.loading-more {
  text-align: center;
  margin-top: 20px;
  color: #808080;
}

.no-more-topics {
  text-align: center;
  margin-top: 30px;
  color: #808080;
  font-style: italic;
}

.spinner {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  animation: spin 1s linear infinite;
  margin: 100px auto;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

If someone knows how to use the normal Discourse view instead of using an HTML custom page, that would be great! I’m happy with the feature itself, but having the default layout would be better, if someone can share how to?