diff --git a/.github/workflows/daily-rss-rebuild.yml b/.github/workflows/daily-rss-rebuild.yml new file mode 100644 index 0000000..958dfa4 --- /dev/null +++ b/.github/workflows/daily-rss-rebuild.yml @@ -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 diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 22c9657..7ce5dd1 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -25,6 +25,7 @@ const config: Config = { 'docusaurus-plugin-image-zoom', './plugins/docusaurus-plugin-unified-tags', './plugins/docusaurus-plugin-recent-articles', + './plugins/docusaurus-plugin-rss-aggregator', [ './plugins/docusaurus-plugin-plausible-custom', { @@ -32,6 +33,7 @@ const config: Config = { scriptSrc: 'https://plausible.tellserv.fr/js/script.js', }, ], + './docusaurus.config.webpack.js', ], title: 'TellServ Tech Blog', @@ -137,6 +139,7 @@ const config: Config = { }, {to: '/blog', label: 'Blog', position: 'left'}, {to: '/tags', label: 'Tags', position: 'left'}, + {to: '/veille', label: 'Veille', position: 'left'}, {to: '/about', label: 'À propos', position: 'right'}, { type: 'localeDropdown', @@ -154,6 +157,12 @@ const config: Config = { className: 'header-forgejo-link', 'aria-label': 'Forgejo profile', }, + { + href: 'https://docs.tellserv.fr/blog/atom.xml', + position: 'right', + className: 'header-rss-link', + 'aria-label': 'RSS Feed', + }, ], }, footer: { diff --git a/docusaurus.config.webpack.js b/docusaurus.config.webpack.js new file mode 100644 index 0000000..d8bad15 --- /dev/null +++ b/docusaurus.config.webpack.js @@ -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 {}; + }, + }; +}; diff --git a/i18n/en/docusaurus-plugin-content-pages/veille.module.css b/i18n/en/docusaurus-plugin-content-pages/veille.module.css new file mode 100644 index 0000000..89ad2c4 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-pages/veille.module.css @@ -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; + } +} diff --git a/i18n/en/docusaurus-plugin-content-pages/veille.tsx b/i18n/en/docusaurus-plugin-content-pages/veille.tsx new file mode 100644 index 0000000..b29e530 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-pages/veille.tsx @@ -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 ( + +
+
+
+

Tech Watch

+

+ Today's articles from recognized sources in SysAdmin, DevOps, SRE, Cloud, and Cybersecurity +

+ +
+ + Download OPML file + +

+ Import this file into your favorite RSS reader (Feedly, Inoreader, FreshRSS, etc.) +

+
+
+ +
+ +
+
+
+
+ ); +} diff --git a/i18n/en/docusaurus-theme-classic/navbar.json b/i18n/en/docusaurus-theme-classic/navbar.json index 7da6354..37b35c7 100644 --- a/i18n/en/docusaurus-theme-classic/navbar.json +++ b/i18n/en/docusaurus-theme-classic/navbar.json @@ -2,5 +2,9 @@ "item.label.À propos": { "message": "About", "description": "Navbar item label for the About page" + }, + "item.label.Veille": { + "message": "Tech Watch", + "description": "Navbar item label for the Tech Watch page" } } diff --git a/package-lock.json b/package-lock.json index fa7828a..2a0c0b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,10 @@ "@easyops-cn/docusaurus-search-local": "^0.52.1", "clsx": "^2.1.1", "docusaurus-plugin-image-zoom": "^3.0.1", + "fast-xml-parser": "^5.3.2", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "rss-parser": "^3.13.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.9.2", @@ -8617,6 +8619,24 @@ ], "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": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -16158,6 +16178,25 @@ "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": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -17004,6 +17043,18 @@ "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": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.19.tgz", @@ -18569,6 +18620,28 @@ "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": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 0ed169a..3197fc0 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,10 @@ "@easyops-cn/docusaurus-search-local": "^0.52.1", "clsx": "^2.1.1", "docusaurus-plugin-image-zoom": "^3.0.1", + "fast-xml-parser": "^5.3.2", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "rss-parser": "^3.13.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.9.2", diff --git a/plugins/docusaurus-plugin-rss-aggregator/index.js b/plugins/docusaurus-plugin-rss-aggregator/index.js new file mode 100644 index 0000000..71cf3ba --- /dev/null +++ b/plugins/docusaurus-plugin-rss-aggregator/index.js @@ -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); + }, + }; +}; diff --git a/src/components/RSSFeedWidget/index.tsx b/src/components/RSSFeedWidget/index.tsx new file mode 100644 index 0000000..fa348d6 --- /dev/null +++ b/src/components/RSSFeedWidget/index.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [generatedAt, setGeneratedAt] = useState(''); + const [expandedCategories, setExpandedCategories] = useState>(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 ( +
+
+

{t.loading}

+
+ ); + } + + if (error) { + return ( +
+

{t.error}

+
+ ); + } + + if (categoryGroups.length === 0) { + return ( +
+

{t.noArticles}

+

{t.comeBack}

+
+ ); + } + + return ( +
+
+

{t.articlesCount(getTotalArticles())}

+
+ + {categoryGroups.map((group) => { + const isExpanded = expandedCategories.has(group.category); + return ( +
+

toggleCategory(group.category)} + style={{ cursor: 'pointer', userSelect: 'none' }} + > + + {isExpanded ? '▼' : '▶'} + {group.category} + + {group.items.length} +

+ + {isExpanded && ( +
+ {group.items.map((item, index) => ( + + ))} +
+ )} +
+ ); + })} +
+ ); +}; + +export default RSSFeedWidget; diff --git a/src/components/RSSFeedWidget/styles.module.css b/src/components/RSSFeedWidget/styles.module.css new file mode 100644 index 0000000..21dc5e1 --- /dev/null +++ b/src/components/RSSFeedWidget/styles.module.css @@ -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; + } +} diff --git a/src/css/custom.css b/src/css/custom.css index d380b87..1221013 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -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 ============================================ */ diff --git a/src/pages/veille.module.css b/src/pages/veille.module.css new file mode 100644 index 0000000..89ad2c4 --- /dev/null +++ b/src/pages/veille.module.css @@ -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; + } +} diff --git a/src/pages/veille.tsx b/src/pages/veille.tsx new file mode 100644 index 0000000..f10b99b --- /dev/null +++ b/src/pages/veille.tsx @@ -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 ( + +
+
+
+

Veille Technologique

+

+ Articles du jour provenant de sources reconnues en SysAdmin, DevOps, SRE, Cloud et Cybersécurité +

+ +
+ + Télécharger le fichier OPML + +

+ Importez ce fichier dans votre lecteur RSS favori (Feedly, Inoreader, FreshRSS, etc.) +

+
+
+ +
+ +
+
+
+
+ ); +} diff --git a/static/img/rss-color-svgrepo-com.svg b/static/img/rss-color-svgrepo-com.svg new file mode 100644 index 0000000..1509b98 --- /dev/null +++ b/static/img/rss-color-svgrepo-com.svg @@ -0,0 +1,17 @@ + + + + + RSS-color + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/static/veille-tech.opml b/static/veille-tech.opml new file mode 100644 index 0000000..cbba317 --- /dev/null +++ b/static/veille-tech.opml @@ -0,0 +1,59 @@ + + + + Veille Technologique (SysAdmin, SRE, GitOps) + Fri, 05 Dec 2025 12:00:00 GMT + Gemini Assistant + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +