Категория, загружающая случайные темы?

Я попытался найти информацию в Google, но ничего подходящего не нашёл.

Думаю, некоторые темы могут быть очень ценными, но пользователи склонны уделять больше внимания самым свежим (если только они не попали на форум через поиск в Google, например).

Возможно ли, возможно, с помощью компонента или плагина, создать категорию или что-то подобное, где автоматически загружались бы случайные темы? Это помогло бы возродить старые посты, которые, возможно, три года назад не пользовались большим вниманием, но всё же были ценными, и, возможно, сейчас, при большем числе пользователей, они получили бы заслуженное внимание.

Для ясности: я не хочу, чтобы случайные темы ограничивались одной категорией, хотя это тоже могло бы быть полезно. Я представлял себе скорее глобальную функцию, где любая тема из любой категории могла бы загружаться в этот раздел. В принципе, так же, как когда я захожу на главную страницу https://meta.discourse.org/, где вижу список всех самых свежих тем, независимо от категории, но в моём случае загружались бы случайные темы из любой категории, без привязки к дате публикации или последним ответам.

Надеюсь, идея понятна…

Мне нравится идея случайных тем, но, полагаю, её стоит доработать. Мой форум существует уже более 20 лет, и было бы странно видеть страницу с кучей случайных тем, которым больше десяти лет.

Если вы хотите ориентироваться на темы с низкой активностью, должен быть параметр, например, максимальное количество ответов для отображения.

Даже в этом случае это не означает, что все такие темы с низкой активностью будут интересными.

Вы хотите ориентироваться на темы по каким-то критериям или просто… на случайные?

Самое близкое, что существует, находится в настройках категории:

Это (возможно, всё ещё) используется здесь в канале Support, поскольку идеальное состояние тем в Support — это решение/закрытие. Открытые темы здесь обычно означают, что решение не было предоставлено, что даёт шанс обратить внимание на темы с низкой активностью, даже если прошло несколько месяцев или лет — это всегда может быть ценно.

Случайные.

Это могла бы быть, например, конкретная категория или даже кнопка «Случайная тема».
Или, возможно, просто раздел внизу списка последних сообщений, который загружал бы 3 или 4 старых случайных поста?

Даже посты с вашего форума, которому более 20 лет, могут быть актуальны и сегодня. Возможно, когда они были созданы, на них никто не обратил внимания, но сейчас они могли бы получить немного «любви» :wink:

Сегодня я решил попросить Claude помочь мне с этим.
Вот что у нас получилось после множества тестов и исправлений (загружаются только открытые темы, что кажется мне более логичным):

При нажатии на кнопку «Загрузить ещё случайные темы» ниже текущих загружается ещё 5 тем. Повторное нажатие добавляет ещё 5, пока темы не закончатся:

Чтобы загрузить случайные темы, просто перейдите по адресу:
вашсайт.com/?random


Вот как это реализовать:

1 — Создайте компонент.
2 — Добавьте следующее во вкладку JS:

import { apiInitializer } from "discourse/lib/api";
// Скрыть содержимое сразу, если в URL есть ?random
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) {
  // Скрыть кнопку и показать индикатор загрузки
  button.style.display = 'none';
  const loadingDiv = document.createElement('div');
  loadingDiv.className = 'loading-more';
  loadingDiv.textContent = 'Загрузка...';
  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 = 'Больше тем для загрузки нет';
        listDiv.appendChild(noMoreDiv);
        return;
      }
      
      // Исключить уже загруженные темы
      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 = 'Больше тем для загрузки нет';
        listDiv.appendChild(noMoreDiv);
        return;
      }
      
      // Перемешать и выбрать до 5 случайных тем
      const shuffled = unloadedTopics.sort(() => 0.5 - Math.random());
      const selected = shuffled.slice(0, Math.min(5, unloadedTopics.length));
      
      // Получить категории из данных сайта Discourse
      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 || 'Без категории';
        
        metaDiv.textContent = `${categoryName} • ${formattedDate}`;
        
        itemDiv.appendChild(linkDiv);
        itemDiv.appendChild(metaDiv);
        
        // Вставить перед кнопкой
        listDiv.insertBefore(itemDiv, button);
      });
      
      // Проверить, загрузили ли мы все доступные темы
      if (selected.length < 5 || loadedTopicIds.size >= data.topics.length) {
        button.remove();
        const noMoreDiv = document.createElement('div');
        noMoreDiv.className = 'no-more-topics';
        noMoreDiv.textContent = 'Больше тем для загрузки нет';
        listDiv.appendChild(noMoreDiv);
      } else {
        button.style.display = 'block';
      }
    })
    .catch((err) => {
      console.error("Ошибка загрузки:", err);
      loadingDiv.remove();
      button.style.display = 'block';
    });
}

function loadRandomTopics(container) {
  // Сбросить загруженные темы
  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>Темы не найдены</p>';
        return;
      }
      
      // Перемешать и выбрать 5 случайных тем
      const shuffled = data.topics.sort(() => 0.5 - Math.random());
      const selected = shuffled.slice(0, 5);
      
      // Построить HTML
      const listDiv = document.createElement('div');
      listDiv.className = 'random-topics-list';
      
      const heading = document.createElement('h2');
      heading.textContent = 'Случайные открытые темы';
      listDiv.appendChild(heading);
      
      // Получить категории из данных сайта Discourse
      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 || 'Без категории';
        
        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 = 'Загрузить ещё случайные темы';
      button.addEventListener('click', () => {
        addRandomTopics(listDiv, button);
      });
      
      listDiv.appendChild(button);
      container.innerHTML = '';
      container.appendChild(listDiv);
    })
    .catch((err) => {
      console.error("Ошибка загрузки:", err);
      container.innerHTML = '<p>Ошибка при загрузке тем</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 (wasOnRandomPage && !isOnRandomPage) {
      window.location.reload();
      return;
    }
    
    wasOnRandomPage = isOnRandomPage;
    
    if (!isOnRandomPage) {
      return;
    }
    
    // Показать и заменить содержимое
    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 — Добавьте следующее во вкладку CSS:

.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); }
}

Если кто-то знает, как использовать обычный вид Discourse вместо создания пользовательской HTML-страницы, буду рад узнать! Сама функция меня устраивает, но было бы лучше, если бы использовалась стандартная разметка. Если кто-то может поделиться решением — пожалуйста!}