Aujourd’hui, j’ai décidé de demander à Claude de m’aider avec ceci.
Voici ce que nous avons trouvé, après beaucoup de tests et de corrections (il ne charge que les sujets OUVERTS, ce qui a plus de sens pour moi) :

Lorsque vous cliquez sur « Charger plus de sujets aléatoires », 5 sujets supplémentaires sont chargés sous ceux qui sont actuellement affichés. Cliquer à nouveau sur le bouton ajoute 5 sujets de plus, jusqu’à ce qu’il n’y ait plus de sujets :

Pour charger les sujets aléatoires, visitez simplement :
votresite.com/?random
Voici comment l’implémenter :
1 - Créez un composant
2 - Ajoutez ceci dans l’onglet JS :
import { apiInitializer } from "discourse/lib/api";
// Masquer le contenu immédiatement si ?random est dans l'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) {
// Masquer le bouton et afficher le chargement
button.style.display = 'none';
const loadingDiv = document.createElement('div');
loadingDiv.className = 'loading-more';
loadingDiv.textContent = 'Chargement...';
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 = 'Plus de sujets à charger';
listDiv.appendChild(noMoreDiv);
return;
}
// Filtrer les sujets déjà chargés
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 = 'Plus de sujets à charger';
listDiv.appendChild(noMoreDiv);
return;
}
// Mélanger et sélectionner jusqu'à 5 sujets aléatoires
const shuffled = unloadedTopics.sort(() => 0.5 - Math.random());
const selected = shuffled.slice(0, Math.min(5, unloadedTopics.length));
// Obtenir la catégorie à partir des données du site 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 || 'Uncategorized';
metaDiv.textContent = `${categoryName} • ${formattedDate}`;
itemDiv.appendChild(linkDiv);
itemDiv.appendChild(metaDiv);
// Insérer avant le bouton
listDiv.insertBefore(itemDiv, button);
});
// Vérifier si nous avons chargé tous les sujets disponibles
if (selected.length < 5 || loadedTopicIds.size >= data.topics.length) {
button.remove();
const noMoreDiv = document.createElement('div');
noMoreDiv.className = 'no-more-topics';
noMoreDiv.textContent = 'Plus de sujets à charger';
listDiv.appendChild(noMoreDiv);
} else {
button.style.display = 'block';
}
})
.catch((err) => {
console.error("Fetch error:", err);
loadingDiv.remove();
button.style.display = 'block';
});
}
function loadRandomTopics(container) {
// Réinitialiser les sujets chargés
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>Aucun sujet trouvé</p>';
return;
}
// Mélanger et sélectionner 5 sujets aléatoires
const shuffled = data.topics.sort(() => 0.5 - Math.random());
const selected = shuffled.slice(0, 5);
// Construire le HTML
const listDiv = document.createElement('div');
listDiv.className = 'random-topics-list';
const heading = document.createElement('h2');
heading.textContent = 'Sujets ouverts aléatoires';
listDiv.appendChild(heading);
// Obtenir la catégorie à partir des données du site 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 || '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 = 'Charger plus de sujets aléatoires';
button.addEventListener('click', () => {
addRandomTopics(listDiv, button);
});
listDiv.appendChild(button);
container.innerHTML = '';
container.appendChild(listDiv);
})
.catch((err) => {
console.error("Fetch error:", err);
container.innerHTML = '<p>Erreur lors du chargement des sujets</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');
// Si nous étions sur la page aléatoire mais que nous n'y sommes plus, forcer le rechargement
if (wasOnRandomPage && !isOnRandomPage) {
window.location.reload();
return;
}
wasOnRandomPage = isOnRandomPage;
if (!isOnRandomPage) {
return;
}
// Afficher et remplacer le contenu
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 - Ajoutez ceci dans l’onglet 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); }
}
Si quelqu’un sait comment utiliser la vue Discourse normale au lieu d’utiliser une page personnalisée HTML, ce serait génial ! Je suis satisfait de la fonctionnalité elle-même, mais avoir la mise en page par défaut serait mieux, si quelqu’un peut partager comment faire ?