Ajout page Veille avec agrégation RSS automatique
- 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.
This commit is contained in:
parent
aaf03916d4
commit
df63713055
16 changed files with 1148 additions and 5 deletions
165
src/components/RSSFeedWidget/index.tsx
Normal file
165
src/components/RSSFeedWidget/index.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation } from '@docusaurus/router';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
interface FeedItem {
|
||||
title: string;
|
||||
link: string;
|
||||
pubDate: string;
|
||||
source: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface CategoryGroup {
|
||||
category: string;
|
||||
items: FeedItem[];
|
||||
}
|
||||
|
||||
interface RSSCacheData {
|
||||
groups: CategoryGroup[];
|
||||
generatedAt: string;
|
||||
totalArticles: number;
|
||||
}
|
||||
|
||||
const RSSFeedWidget: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const isEnglish = location.pathname.startsWith('/en');
|
||||
const [categoryGroups, setCategoryGroups] = useState<CategoryGroup[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [generatedAt, setGeneratedAt] = useState<string>('');
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||
|
||||
const t = {
|
||||
loading: isEnglish ? 'Loading RSS feeds...' : 'Chargement des flux RSS...',
|
||||
error: isEnglish ? 'Error loading RSS feeds' : 'Erreur lors du chargement des flux RSS',
|
||||
noArticles: isEnglish ? 'No articles published in the last 24 hours in monitored RSS feeds.' : 'Aucun article publié dans les dernières 24h dans les flux RSS suivis.',
|
||||
comeBack: isEnglish ? 'Come back later for new updates!' : 'Revenez plus tard pour de nouvelles actualités !',
|
||||
articlesCount: (count: number) => isEnglish
|
||||
? `${count} article${count > 1 ? 's' : ''} in the last 24 hours`
|
||||
: `${count} article${count > 1 ? 's' : ''} publié${count > 1 ? 's' : ''} dans les dernières 24h`,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFeeds = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Chargement du fichier JSON pré-généré au build
|
||||
const response = await fetch('/rss-feed-cache.json');
|
||||
const data: RSSCacheData = await response.json();
|
||||
|
||||
setCategoryGroups(data.groups);
|
||||
setGeneratedAt(data.generatedAt);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error('Erreur lors du chargement des flux RSS:', err);
|
||||
setError('Erreur lors du chargement des flux RSS');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFeeds();
|
||||
}, []);
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleTimeString(isEnglish ? 'en-US' : 'fr-FR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const getTotalArticles = () => {
|
||||
return categoryGroups.reduce((total, group) => total + group.items.length, 0);
|
||||
};
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setExpandedCategories((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(category)) {
|
||||
newSet.delete(category);
|
||||
} else {
|
||||
newSet.add(category);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
<div className={styles.spinner}></div>
|
||||
<p>{t.loading}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<p>{t.error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (categoryGroups.length === 0) {
|
||||
return (
|
||||
<div className={styles.noArticles}>
|
||||
<p>{t.noArticles}</p>
|
||||
<p>{t.comeBack}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.summary}>
|
||||
<p>{t.articlesCount(getTotalArticles())}</p>
|
||||
</div>
|
||||
|
||||
{categoryGroups.map((group) => {
|
||||
const isExpanded = expandedCategories.has(group.category);
|
||||
return (
|
||||
<div key={group.category} className={styles.categorySection}>
|
||||
<h2
|
||||
className={styles.categoryTitle}
|
||||
onClick={() => toggleCategory(group.category)}
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
>
|
||||
<span className={styles.categoryTitleContent}>
|
||||
<span className={styles.expandIcon}>{isExpanded ? '▼' : '▶'}</span>
|
||||
{group.category}
|
||||
</span>
|
||||
<span className={styles.categoryCount}>{group.items.length}</span>
|
||||
</h2>
|
||||
|
||||
{isExpanded && (
|
||||
<div className={styles.feedList}>
|
||||
{group.items.map((item, index) => (
|
||||
<article key={`${item.link}-${index}`} className={styles.feedItem}>
|
||||
<div className={styles.feedHeader}>
|
||||
<span className={styles.source}>{item.source}</span>
|
||||
<time className={styles.date}>{formatDate(item.pubDate)}</time>
|
||||
</div>
|
||||
<h3 className={styles.title}>
|
||||
<a href={item.link} target="_blank" rel="noopener noreferrer">
|
||||
{item.title}
|
||||
</a>
|
||||
</h3>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RSSFeedWidget;
|
||||
212
src/components/RSSFeedWidget/styles.module.css
Normal file
212
src/components/RSSFeedWidget/styles.module.css
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
/* Conteneur principal */
|
||||
.container {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Résumé du jour */
|
||||
.summary {
|
||||
padding: 1rem 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
background: linear-gradient(135deg, var(--ifm-color-primary-lightest) 0%, var(--ifm-color-primary-lighter) 100%);
|
||||
border-left: 4px solid var(--ifm-color-primary);
|
||||
border-radius: 8px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ifm-color-primary-darker);
|
||||
}
|
||||
|
||||
.summary p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Section par catégorie */
|
||||
.categorySection {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.categoryTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
border: 2px solid var(--ifm-color-primary-lighter);
|
||||
border-radius: 8px;
|
||||
font-size: 1.5rem;
|
||||
color: var(--ifm-color-content);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.categoryTitle:hover {
|
||||
background-color: var(--ifm-color-primary-lightest);
|
||||
border-color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.categoryTitleContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
color: var(--ifm-color-primary);
|
||||
font-size: 0.9rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.categoryCount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0 0.75rem;
|
||||
background-color: var(--ifm-color-primary);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Liste des flux */
|
||||
.feedList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Article individuel */
|
||||
.feedItem {
|
||||
padding: 1.5rem;
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feedItem:hover {
|
||||
border-color: var(--ifm-color-primary);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* En-tête de l'article */
|
||||
.feedHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.source {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background-color: var(--ifm-color-emphasis-100);
|
||||
color: var(--ifm-color-content-secondary);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Titre de l'article */
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.title a {
|
||||
color: var(--ifm-color-content);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.title a:hover {
|
||||
color: var(--ifm-color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* État de chargement */
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--ifm-color-emphasis-300);
|
||||
border-top-color: var(--ifm-color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* État d'erreur */
|
||||
.error {
|
||||
padding: 2rem;
|
||||
background-color: var(--ifm-color-danger-contrast-background);
|
||||
border: 1px solid var(--ifm-color-danger);
|
||||
border-radius: 8px;
|
||||
color: var(--ifm-color-danger-darker);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Aucun article */
|
||||
.noArticles {
|
||||
padding: 3rem 2rem;
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
border: 2px dashed var(--ifm-color-emphasis-300);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
}
|
||||
|
||||
.noArticles p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.categoryTitle {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.feedItem {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.summary {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,8 @@
|
|||
============================================ */
|
||||
|
||||
.header-github-link::before,
|
||||
.header-forgejo-link::before {
|
||||
.header-forgejo-link::before,
|
||||
.header-rss-link::before {
|
||||
content: '';
|
||||
display: flex;
|
||||
width: 24px;
|
||||
|
|
@ -47,9 +48,15 @@
|
|||
background-image: url('/img/forgejo-logo.svg');
|
||||
}
|
||||
|
||||
/* RSS icon */
|
||||
.header-rss-link::before {
|
||||
background-image: url('/img/rss-color-svgrepo-com.svg');
|
||||
}
|
||||
|
||||
/* Hide the default link text (if any) */
|
||||
.header-github-link > span,
|
||||
.header-forgejo-link > span {
|
||||
.header-forgejo-link > span,
|
||||
.header-rss-link > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +75,8 @@
|
|||
============================================ */
|
||||
|
||||
.header-github-link:hover::before,
|
||||
.header-forgejo-link:hover::before {
|
||||
.header-forgejo-link:hover::before,
|
||||
.header-rss-link:hover::before {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +98,15 @@
|
|||
filter: drop-shadow(0 0 8px rgba(255, 102, 0, 0.8));
|
||||
}
|
||||
|
||||
/* RSS hover glow effect - orange RSS color */
|
||||
.header-rss-link:hover::before {
|
||||
filter: drop-shadow(0 0 6px rgba(255, 153, 0, 0.7));
|
||||
}
|
||||
|
||||
[data-theme='dark'] .header-rss-link:hover::before {
|
||||
filter: drop-shadow(0 0 8px rgba(255, 153, 0, 0.8));
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Search Bar Styling
|
||||
============================================ */
|
||||
|
|
|
|||
155
src/pages/veille.module.css
Normal file
155
src/pages/veille.module.css
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
.veillePage {
|
||||
padding: 2rem 0;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 2px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
max-width: 800px;
|
||||
margin: 0 auto 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.downloadButton {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 2rem;
|
||||
background: var(--ifm-color-primary);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.downloadButton:hover {
|
||||
background: var(--ifm-color-primary-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.opmlInfo {
|
||||
font-size: 0.9rem;
|
||||
color: var(--ifm-color-content-secondary);
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 4rem;
|
||||
padding-top: 3rem;
|
||||
border-top: 2px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
.footer h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--ifm-color-content);
|
||||
}
|
||||
|
||||
.infoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.infoCard {
|
||||
padding: 1.5rem;
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infoCard:hover {
|
||||
border-color: var(--ifm-color-primary);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.infoCard h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--ifm-color-primary);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.infoCard p {
|
||||
margin-bottom: 0;
|
||||
line-height: 1.6;
|
||||
color: var(--ifm-color-content);
|
||||
}
|
||||
|
||||
.infoCard ul {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.infoCard li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--ifm-color-content);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 996px) {
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.infoGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.veillePage {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.downloadButton {
|
||||
padding: 0.6rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
39
src/pages/veille.tsx
Normal file
39
src/pages/veille.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import RSSFeedWidget from '@site/src/components/RSSFeedWidget';
|
||||
import styles from './veille.module.css';
|
||||
|
||||
export default function Veille(): JSX.Element {
|
||||
return (
|
||||
<Layout
|
||||
title="Veille Technologique"
|
||||
description="Flux RSS quotidiens sur le SysAdmin, DevOps, SRE, Cloud et Sécurité">
|
||||
<main className={styles.veillePage}>
|
||||
<div className="container">
|
||||
<header className={styles.header}>
|
||||
<h1>Veille Technologique</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Articles du jour provenant de sources reconnues en SysAdmin, DevOps, SRE, Cloud et Cybersécurité
|
||||
</p>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<a
|
||||
href="/veille-tech.opml"
|
||||
download="veille-tech.opml"
|
||||
className={styles.downloadButton}>
|
||||
Télécharger le fichier OPML
|
||||
</a>
|
||||
<p className={styles.opmlInfo}>
|
||||
Importez ce fichier dans votre lecteur RSS favori (Feedly, Inoreader, FreshRSS, etc.)
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className={styles.content}>
|
||||
<RSSFeedWidget />
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue