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:
Tellsanguis 2025-12-06 09:33:43 +01:00
parent aaf03916d4
commit df63713055
16 changed files with 1148 additions and 5 deletions

42
.github/workflows/daily-rss-rebuild.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Daily RSS Feed Rebuild
on:
schedule:
# Tous les jours à 6h UTC (7h CET / 8h CEST)
- cron: '0 6 * * *'
workflow_dispatch: # Permet de déclencher manuellement le workflow
jobs:
trigger-rebuild:
runs-on: ubuntu-latest
steps:
- name: Déclencher le rebuild Cloudflare Pages
run: |
echo "Déclenchement du rebuild pour mettre à jour les flux RSS..."
# Récupération du dernier commit pour le déploiement
RESPONSE=$(curl -s -X POST \
"https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/pages/projects/${{ secrets.CLOUDFLARE_PROJECT_NAME }}/deployments" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type: application/json" \
--data '{
"branch": "main"
}')
echo "$RESPONSE"
# Vérification du succès
if echo "$RESPONSE" | grep -q '"success":true'; then
echo "✅ Rebuild Cloudflare Pages déclenché avec succès"
else
echo "❌ Échec du déclenchement du rebuild"
echo "$RESPONSE"
exit 1
fi
- name: Résumé
run: |
echo "## Rebuild quotidien des flux RSS" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Le rebuild a été déclenché avec succès sur Cloudflare Pages." >> $GITHUB_STEP_SUMMARY
echo "Les flux RSS seront mis à jour avec les articles des dernières 24h." >> $GITHUB_STEP_SUMMARY

View file

@ -25,6 +25,7 @@ const config: Config = {
'docusaurus-plugin-image-zoom', 'docusaurus-plugin-image-zoom',
'./plugins/docusaurus-plugin-unified-tags', './plugins/docusaurus-plugin-unified-tags',
'./plugins/docusaurus-plugin-recent-articles', './plugins/docusaurus-plugin-recent-articles',
'./plugins/docusaurus-plugin-rss-aggregator',
[ [
'./plugins/docusaurus-plugin-plausible-custom', './plugins/docusaurus-plugin-plausible-custom',
{ {
@ -32,6 +33,7 @@ const config: Config = {
scriptSrc: 'https://plausible.tellserv.fr/js/script.js', scriptSrc: 'https://plausible.tellserv.fr/js/script.js',
}, },
], ],
'./docusaurus.config.webpack.js',
], ],
title: 'TellServ Tech Blog', title: 'TellServ Tech Blog',
@ -137,6 +139,7 @@ const config: Config = {
}, },
{to: '/blog', label: 'Blog', position: 'left'}, {to: '/blog', label: 'Blog', position: 'left'},
{to: '/tags', label: 'Tags', position: 'left'}, {to: '/tags', label: 'Tags', position: 'left'},
{to: '/veille', label: 'Veille', position: 'left'},
{to: '/about', label: 'À propos', position: 'right'}, {to: '/about', label: 'À propos', position: 'right'},
{ {
type: 'localeDropdown', type: 'localeDropdown',
@ -154,6 +157,12 @@ const config: Config = {
className: 'header-forgejo-link', className: 'header-forgejo-link',
'aria-label': 'Forgejo profile', 'aria-label': 'Forgejo profile',
}, },
{
href: 'https://docs.tellserv.fr/blog/atom.xml',
position: 'right',
className: 'header-rss-link',
'aria-label': 'RSS Feed',
},
], ],
}, },
footer: { footer: {

View file

@ -0,0 +1,22 @@
module.exports = function (context, options) {
return {
name: 'custom-webpack-config',
configureWebpack(config, isServer) {
if (!isServer) {
return {
resolve: {
fallback: {
http: false,
https: false,
url: false,
buffer: false,
timers: false,
stream: false,
},
},
};
}
return {};
},
};
};

View 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;
}
}

View 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="Tech Watch"
description="Daily RSS feeds on SysAdmin, DevOps, SRE, Cloud, and Security">
<main className={styles.veillePage}>
<div className="container">
<header className={styles.header}>
<h1>Tech Watch</h1>
<p className={styles.subtitle}>
Today's articles from recognized sources in SysAdmin, DevOps, SRE, Cloud, and Cybersecurity
</p>
<div className={styles.actions}>
<a
href="/veille-tech.opml"
download="veille-tech.opml"
className={styles.downloadButton}>
Download OPML file
</a>
<p className={styles.opmlInfo}>
Import this file into your favorite RSS reader (Feedly, Inoreader, FreshRSS, etc.)
</p>
</div>
</header>
<section className={styles.content}>
<RSSFeedWidget />
</section>
</div>
</main>
</Layout>
);
}

View file

@ -2,5 +2,9 @@
"item.label.À propos": { "item.label.À propos": {
"message": "About", "message": "About",
"description": "Navbar item label for the About page" "description": "Navbar item label for the About page"
},
"item.label.Veille": {
"message": "Tech Watch",
"description": "Navbar item label for the Tech Watch page"
} }
} }

75
package-lock.json generated
View file

@ -14,8 +14,10 @@
"@easyops-cn/docusaurus-search-local": "^0.52.1", "@easyops-cn/docusaurus-search-local": "^0.52.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"docusaurus-plugin-image-zoom": "^3.0.1", "docusaurus-plugin-image-zoom": "^3.0.1",
"fast-xml-parser": "^5.3.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1",
"rss-parser": "^3.13.0"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "^3.9.2", "@docusaurus/module-type-aliases": "^3.9.2",
@ -8617,6 +8619,24 @@
], ],
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/fast-xml-parser": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.2.tgz",
"integrity": "sha512-n8v8b6p4Z1sMgqRmqLJm3awW4NX7NkaKPfb3uJIBTSH7Pdvufi3PQ3/lJLQrvxcMYl7JI2jnDO90siPEpD8JBA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^2.1.0"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@ -16158,6 +16178,25 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rss-parser": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz",
"integrity": "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==",
"license": "MIT",
"dependencies": {
"entities": "^2.0.3",
"xml2js": "^0.5.0"
}
},
"node_modules/rss-parser/node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"license": "BSD-2-Clause",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/rtlcss": { "node_modules/rtlcss": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz",
@ -17004,6 +17043,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/strnum": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
"integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/style-to-js": { "node_modules/style-to-js": {
"version": "1.1.19", "version": "1.1.19",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.19.tgz", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.19.tgz",
@ -18569,6 +18620,28 @@
"xml-js": "bin/cli.js" "xml-js": "bin/cli.js"
} }
}, },
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View file

@ -27,8 +27,10 @@
"@easyops-cn/docusaurus-search-local": "^0.52.1", "@easyops-cn/docusaurus-search-local": "^0.52.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"docusaurus-plugin-image-zoom": "^3.0.1", "docusaurus-plugin-image-zoom": "^3.0.1",
"fast-xml-parser": "^5.3.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1",
"rss-parser": "^3.13.0"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "^3.9.2", "@docusaurus/module-type-aliases": "^3.9.2",

View file

@ -0,0 +1,133 @@
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);
},
};
};

View 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;

View 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;
}
}

View file

@ -26,7 +26,8 @@
============================================ */ ============================================ */
.header-github-link::before, .header-github-link::before,
.header-forgejo-link::before { .header-forgejo-link::before,
.header-rss-link::before {
content: ''; content: '';
display: flex; display: flex;
width: 24px; width: 24px;
@ -47,9 +48,15 @@
background-image: url('/img/forgejo-logo.svg'); 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) */ /* Hide the default link text (if any) */
.header-github-link > span, .header-github-link > span,
.header-forgejo-link > span { .header-forgejo-link > span,
.header-rss-link > span {
display: none; display: none;
} }
@ -68,7 +75,8 @@
============================================ */ ============================================ */
.header-github-link:hover::before, .header-github-link:hover::before,
.header-forgejo-link:hover::before { .header-forgejo-link:hover::before,
.header-rss-link:hover::before {
opacity: 0.8; opacity: 0.8;
} }
@ -90,6 +98,15 @@
filter: drop-shadow(0 0 8px rgba(255, 102, 0, 0.8)); 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 Search Bar Styling
============================================ */ ============================================ */

155
src/pages/veille.module.css Normal file
View 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
View 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>
);
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 44 44" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>RSS-color</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Color-" transform="translate(-800.000000, -760.000000)" fill="#FF9A00">
<path d="M800.000471,797.714286 C800.000471,794.243 802.81487,791.428571 806.286118,791.428571 C809.757367,791.428571 812.571765,794.243 812.571765,797.714286 C812.571765,801.185571 809.757367,804 806.286118,804 C802.81487,804 800.000471,801.185571 800.000471,797.714286 Z M844,804 L835.619661,804 C835.619661,784.358714 819.641547,768.380429 800.000471,768.380429 L800.000471,760 C824.261497,760 844,779.738714 844,804 Z M829.333543,804 L820.953204,804 C820.953204,792.446857 811.553019,783.048143 800,783.048143 L800,774.666143 C816.174541,774.666143 829.333543,787.825286 829.333543,804 Z" id="RSS">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

59
static/veille-tech.opml Normal file
View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head>
<title>Veille Technologique (SysAdmin, SRE, GitOps)</title>
<dateCreated>Fri, 05 Dec 2025 12:00:00 GMT</dateCreated>
<ownerName>Gemini Assistant</ownerName>
</head>
<body>
<outline text="Systèmes / Linux / Infra" title="Systèmes / Linux / Infra">
<outline type="rss" text="LWN.net (Linux Kernel)" title="LWN.net (Linux Kernel)" xmlUrl="https://lwn.net/headlines/rss" />
<outline type="rss" text="Brendan Gregg's Blog (Performance)" title="Brendan Gregg's Blog (Performance)" xmlUrl="http://www.brendangregg.com/blog/rss.xml" />
<outline type="rss" text="Phoronix (Linux Hardware &amp; Benchmarks)" title="Phoronix (Linux Hardware &amp; Benchmarks)" xmlUrl="https://www.phoronix.com/rss.php" />
<outline type="rss" text="Arch Linux News" title="Arch Linux News" xmlUrl="https://archlinux.org/feeds/news/" />
<outline type="rss" text="Stéphane Bortzmeyer (FR)" title="Stéphane Bortzmeyer (FR)" xmlUrl="http://www.bortzmeyer.org/feed.atom" />
<outline type="rss" text="LinuxFR.org" title="LinuxFR.org" xmlUrl="https://linuxfr.org/news.atom" />
</outline>
<outline text="SRE / DevOps / GitOps" title="SRE / DevOps / GitOps">
<outline type="rss" text="Google SRE Blog" title="Google SRE Blog" xmlUrl="https://cloudblog.withgoogle.com/rss" />
<outline type="rss" text="Netflix Technology Blog" title="Netflix Technology Blog" xmlUrl="http://techblog.netflix.com/feeds/posts/default?alt=rss" />
<outline type="rss" text="GitLab Blog" title="GitLab Blog" xmlUrl="https://about.gitlab.com/atom.xml" />
<outline type="rss" text="DevOps.com" title="DevOps.com" xmlUrl="https://devops.com/feed/" />
<outline type="rss" text="The Register - DevOps" title="The Register - DevOps" xmlUrl="https://www.theregister.com/devops/headlines.atom" />
</outline>
<outline text="Kubernetes / Cloud / Conteneurs" title="Kubernetes / Cloud / Conteneurs">
<outline type="rss" text="Kubernetes Blog" title="Kubernetes Blog" xmlUrl="https://kubernetes.io/feed.xml" />
<outline type="rss" text="AWS Blog" title="AWS Blog" xmlUrl="https://aws.amazon.com/blogs/aws/feed/" />
<outline type="rss" text="Google Cloud Blog" title="Google Cloud Blog" xmlUrl="https://cloudblog.withgoogle.com/rss" />
<outline type="rss" text="Azure Blog" title="Azure Blog" xmlUrl="https://azure.microsoft.com/fr-fr/blog/feed/" />
<outline type="rss" text="Docker Blog" title="Docker Blog" xmlUrl="https://www.docker.com/blog/feed/" />
<outline type="rss" text="Red Hat Blog" title="Red Hat Blog" xmlUrl="https://www.redhat.com/en/rss/blog" />
<outline type="rss" text="HashiCorp Blog" title="HashiCorp Blog" xmlUrl="https://www.hashicorp.com/blog/feed.xml" />
<outline type="rss" text="OVHcloud Blog (FR)" title="OVHcloud Blog (FR)" xmlUrl="https://blog.ovhcloud.com/category/ovhcloud-en-francais/feed/" />
</outline>
<outline text="Sécurité / Vulnérabilités / Cyber" title="Sécurité / Vulnérabilités / Cyber">
<outline type="rss" text="The Hacker News" title="The Hacker News" xmlUrl="http://www.thehackernews.com/feeds/posts/default" />
<outline type="rss" text="Krebs on Security" title="Krebs on Security" xmlUrl="https://krebsonsecurity.com/feed/" />
<outline type="rss" text="Schneier on Security" title="Schneier on Security" xmlUrl="https://www.schneier.com/feed/" />
<outline type="rss"text="Naked Security by Sophos" title="Naked Security by Sophos" xmlUrl="https://nakedsecurity.sophos.com/feed/" />
<outline type="rss" text="Dark Reading" title="Dark Reading" xmlUrl="https://www.darkreading.com/rss.xml" />
<outline type="rss" text="CERT-FR (ANSSI)" title="CERT-FR (ANSSI)" xmlUrl="https://www.cert.ssi.gouv.fr/feed/" />
</outline>
<outline text="Réseaux / Matériel / Automatisation" title="Réseaux / Matériel / Automatisation">
<outline type="rss" text="Cloudflare Blog" title="Cloudflare Blog" xmlUrl="https://blog.cloudflare.com/rss/" />
<outline type="rss" text="ipspace.net by Ivan Pepelnjak" title="ipspace.net by Ivan Pepelnjak" xmlUrl="https://blog.ipspace.net/atom.xml" />
<outline type="rss" text="Ansible Announcements" title="Ansible Announcements" xmlUrl="https://announcements.ansiblecloud.redhat.com/feed.atom" />
</outline>
<outline text="Veille généraliste Tech / Professionnelle" title="Veille généraliste Tech / Professionnelle">
<outline type="rss" text="Hacker News" title="Hacker News" xmlUrl="https://news.ycombinator.com/rss" />
<outline type="rss" text="Lobste.rs" title="Lobste.rs" xmlUrl="https://lobste.rs/rss" />
<outline type="rss" text="Ars Technica" title="Ars Technica" xmlUrl="http://feeds.arstechnica.com/arstechnica/index/" />
<outline type="rss" text="Le Journal du Hacker (FR)" title="Le Journal du Hacker (FR)" xmlUrl="https://www.journalduhacker.net/rss" />
<outline type="rss" text="Next INpact (FR)" title="Next INpact (FR)" xmlUrl="https://next.ink/feed" />
<outline type="rss" text="The Pragmatic Engineer" title="The Pragmatic Engineer" xmlUrl="https://blog.pragmaticengineer.com/rss/" />
<outline type="rss" text="Korben (FR)" title="Korben (FR)" xmlUrl="https://korben.info/feed" />
<outline type="rss" text="Developpez.com (FR)" title="Developpez.com (FR)" xmlUrl="https://www.developpez.com/index/rss" />
<outline type="rss" text="Le Monde Informatique (FR)" title="Le Monde Informatique (FR)" xmlUrl="https://www.lemondeinformatique.fr/flux-rss/thematique/toute-l-actualite/rss.xml" />
</outline>
</body>
</opml>