- Création d'un plugin Docusaurus pour agréger les flux RSS au build * Récupère 37 flux RSS depuis le fichier OPML * Filtre les articles des dernières 24h * Génère un fichier JSON statique pour chargement instantané - Page Veille avec composant React * Affichage des articles groupés par catégorie * Menus dépliables (repliés par défaut) * Chargement ultra-rapide depuis JSON pré-généré * Support bilingue FR/EN - GitHub Actions pour rebuild automatique quotidien * Workflow déclenché tous les jours à 6h UTC * Met à jour les flux RSS via l'API Cloudflare Pages * Déclenchement manuel possible - Configuration Webpack pour compatibilité navigateur * Désactivation des polyfills Node.js côté client * Correction du warning onBrokenMarkdownLinks - Icône RSS dans la navbar * Lien vers le flux Atom du blog * Style cohérent avec les autres icônes 125 articles trouvés dans les dernières 24h lors du dernier build.
133 lines
4.4 KiB
JavaScript
133 lines
4.4 KiB
JavaScript
const Parser = require('rss-parser');
|
|
const { XMLParser } = require('fast-xml-parser');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
const http = require('http');
|
|
|
|
module.exports = function (context, options) {
|
|
return {
|
|
name: 'docusaurus-plugin-rss-aggregator',
|
|
|
|
async loadContent() {
|
|
console.log('[RSS Aggregator] Récupération des flux RSS...');
|
|
|
|
const parser = new Parser({
|
|
timeout: 10000,
|
|
customFields: {
|
|
item: ['description', 'content:encoded']
|
|
}
|
|
});
|
|
|
|
// Lecture du fichier OPML
|
|
const opmlPath = path.join(context.siteDir, 'static', 'veille-tech.opml');
|
|
const opmlText = fs.readFileSync(opmlPath, 'utf-8');
|
|
|
|
const xmlParser = new XMLParser({
|
|
ignoreAttributes: false,
|
|
attributeNamePrefix: ''
|
|
});
|
|
const opmlData = xmlParser.parse(opmlText);
|
|
|
|
// Extraction des flux depuis le fichier OPML
|
|
const opmlFeeds = [];
|
|
const outlines = opmlData.opml.body.outline;
|
|
|
|
outlines.forEach((categoryOutline) => {
|
|
const categoryName = categoryOutline.text;
|
|
const feedOutlines = Array.isArray(categoryOutline.outline)
|
|
? categoryOutline.outline
|
|
: [categoryOutline.outline];
|
|
|
|
feedOutlines.forEach((feed) => {
|
|
if (feed.xmlUrl) {
|
|
opmlFeeds.push({
|
|
title: feed.text || feed.title,
|
|
xmlUrl: feed.xmlUrl,
|
|
category: categoryName
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Récupération des flux RSS (articles des dernières 24h)
|
|
const allItems = [];
|
|
const now = Date.now();
|
|
const twentyFourHoursAgo = now - (24 * 60 * 60 * 1000);
|
|
|
|
console.log(`[RSS Aggregator] Récupération de ${opmlFeeds.length} flux RSS...`);
|
|
|
|
// Traitement par lots de 5 flux en parallèle pour ne pas surcharger
|
|
const batchSize = 5;
|
|
for (let i = 0; i < opmlFeeds.length; i += batchSize) {
|
|
const batch = opmlFeeds.slice(i, i + batchSize);
|
|
const batchPromises = batch.map(async (feedInfo) => {
|
|
try {
|
|
const feed = await parser.parseURL(feedInfo.xmlUrl);
|
|
|
|
// Filtrer les articles des dernières 24h
|
|
const recentItems = feed.items.filter((item) => {
|
|
const itemDate = new Date(item.pubDate || item.isoDate || '');
|
|
return itemDate.getTime() >= twentyFourHoursAgo;
|
|
});
|
|
|
|
return recentItems.map((item) => ({
|
|
title: item.title || 'Sans titre',
|
|
link: item.link || '#',
|
|
pubDate: item.pubDate || item.isoDate || new Date().toISOString(),
|
|
source: feedInfo.title,
|
|
category: feedInfo.category
|
|
}));
|
|
} catch (err) {
|
|
console.warn(`[RSS Aggregator] Échec ${feedInfo.title}:`, err.message);
|
|
return [];
|
|
}
|
|
});
|
|
|
|
const batchResults = await Promise.all(batchPromises);
|
|
allItems.push(...batchResults.flat());
|
|
}
|
|
|
|
// Grouper par catégorie et trier
|
|
const groupedByCategory = new Map();
|
|
|
|
allItems.forEach((item) => {
|
|
if (!groupedByCategory.has(item.category)) {
|
|
groupedByCategory.set(item.category, []);
|
|
}
|
|
groupedByCategory.get(item.category).push(item);
|
|
});
|
|
|
|
// Trier les articles de chaque catégorie par date (plus récent en premier)
|
|
const groups = Array.from(groupedByCategory.entries())
|
|
.map(([category, items]) => ({
|
|
category,
|
|
items: items.sort((a, b) =>
|
|
new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime()
|
|
)
|
|
}))
|
|
.sort((a, b) => a.category.localeCompare(b.category));
|
|
|
|
console.log(`[RSS Aggregator] ${allItems.length} articles trouvés dans les dernières 24h`);
|
|
|
|
return {
|
|
groups,
|
|
generatedAt: new Date().toISOString(),
|
|
totalArticles: allItems.length
|
|
};
|
|
},
|
|
|
|
async contentLoaded({ content, actions }) {
|
|
const { setGlobalData } = actions;
|
|
|
|
// Écrire les données dans un fichier JSON statique
|
|
const outputPath = path.join(context.siteDir, 'static', 'rss-feed-cache.json');
|
|
fs.writeFileSync(outputPath, JSON.stringify(content, null, 2));
|
|
|
|
console.log(`[RSS Aggregator] Données écrites dans ${outputPath}`);
|
|
|
|
// Rendre les données disponibles globalement
|
|
setGlobalData(content);
|
|
},
|
|
};
|
|
};
|