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
42
.github/workflows/daily-rss-rebuild.yml
vendored
Normal file
42
.github/workflows/daily-rss-rebuild.yml
vendored
Normal 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
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
22
docusaurus.config.webpack.js
Normal file
22
docusaurus.config.webpack.js
Normal 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 {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
155
i18n/en/docusaurus-plugin-content-pages/veille.module.css
Normal file
155
i18n/en/docusaurus-plugin-content-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
i18n/en/docusaurus-plugin-content-pages/veille.tsx
Normal file
39
i18n/en/docusaurus-plugin-content-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="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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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
75
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
133
plugins/docusaurus-plugin-rss-aggregator/index.js
Normal file
133
plugins/docusaurus-plugin-rss-aggregator/index.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
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-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
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
static/img/rss-color-svgrepo-com.svg
Normal file
17
static/img/rss-color-svgrepo-com.svg
Normal 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
59
static/veille-tech.opml
Normal 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 & Benchmarks)" title="Phoronix (Linux Hardware & 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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue