diff --git a/.astro/content.d.ts b/.astro/content.d.ts index c21ccee..48a5228 100644 --- a/.astro/content.d.ts +++ b/.astro/content.d.ts @@ -160,5 +160,5 @@ declare module 'astro:content' { type AnyEntryMap = ContentEntryMap & DataEntryMap; - export type ContentConfig = typeof import("./../src/content/config.js"); + export type ContentConfig = typeof import("../src/content/config.js"); } diff --git a/.astro/data-store.json b/.astro/data-store.json index 0c0b23d..8d9e3b9 100644 --- a/.astro/data-store.json +++ b/.astro/data-store.json @@ -1 +1 @@ -[["Map",1,2,9,10],"meta::meta",["Map",3,4,5,6,7,8],"astro-version","5.8.1","content-config-digest","5e54967a61329eee","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://proxyfil.fr\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"experimentalDefaultStyles\":true},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"responsiveImages\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false},\"legacy\":{\"collections\":false}}","staticData",["Map",11,12],"allStaticData",{"id":11,"data":13,"filePath":37,"digest":38},{"profileImage":14,"profileAlt":15,"profileLink":16,"profileTitle":17,"profileName":18,"github":19,"githubText":20,"portfolioImage":21,"email":22,"linkedin":23,"instagram":24,"youtube":25,"alias":26,"contactSectionTitle":27,"contactSectionSubtitle":28,"contactSectionButtonText":29,"contactSectionButtonIcon":30,"techsTitle":31,"instagramIconName":32,"youtubeIconName":33,"githubIconName":34,"linkedinIconName":35,"emailIconName":36},"/images/proxyfil.webp","Photo of Pierre-Louis Leclerc (Proxyfil) for the blog","/about-me","DevOps Engineer Student","Pierre-Louis Leclerc","https://github.com/proxyfil","Want to see my code?","/images/portfolio.webp","leclerc.pierre-louis@gmail.com","https://www.linkedin.com/in/pierre-louis-leclerc/","https://www.instagram.com/pierrelouisirl/","https://www.youtube.com/@proxyfil4036","Proxyfil","Ready to take your idea to the next level?","Let's work together.","Contact Me","paperplane","TECHS","instagram","youtube","github","linkedin","envelope","src/content/staticData/allStaticData.json","606acb4ab07cd770"] \ No newline at end of file +[["Map",1,2,9,10],"meta::meta",["Map",3,4,5,6,7,8],"astro-version","5.8.1","content-config-digest","3120b7a6e0893304","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://proxyfil.fr\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"experimentalDefaultStyles\":true},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"responsiveImages\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false},\"legacy\":{\"collections\":false}}","staticData",["Map",11,12],"allStaticData",{"id":11,"data":13,"filePath":37,"digest":38},{"profileImage":14,"profileAlt":15,"profileLink":16,"profileTitle":17,"profileName":18,"github":19,"githubText":20,"portfolioImage":21,"email":22,"linkedin":23,"instagram":24,"youtube":25,"alias":26,"contactSectionTitle":27,"contactSectionSubtitle":28,"contactSectionButtonText":29,"contactSectionButtonIcon":30,"techsTitle":31,"instagramIconName":32,"youtubeIconName":33,"githubIconName":34,"linkedinIconName":35,"emailIconName":36},"/images/proxyfil.webp","Photo of Pierre-Louis Leclerc (Proxyfil) for the blog","/about-me","DevOps Engineer Student","Pierre-Louis Leclerc","https://github.com/proxyfil","Want to see my code?","/images/portfolio.webp","leclerc.pierre-louis@gmail.com","https://www.linkedin.com/in/pierre-louis-leclerc/","https://www.instagram.com/pierrelouisirl/","https://www.youtube.com/@proxyfil4036","Proxyfil","Ready to take your idea to the next level?","Let's work together.","Contact Me","paperplane","TECHS","instagram","youtube","github","linkedin","envelope","src/content/staticData/allStaticData.json","67d2708f51b24e1c"] \ No newline at end of file diff --git a/.astro/settings.json b/.astro/settings.json index edc9747..8115b4c 100644 --- a/.astro/settings.json +++ b/.astro/settings.json @@ -1,5 +1,5 @@ { "_variables": { - "lastUpdateCheck": 1748972451562 + "lastUpdateCheck": 1761163064283 } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fe9102a..fe52502 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "alpinejs": "^3.14.9", "astro": "^5.6.1", "astro-icon": "^1.1.5", + "i18n": "^0.15.2", "preact": "^10.26.2", "prismjs": "^1.30.0", "tailwindcss": "^4.0.8" @@ -1704,6 +1705,50 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@messageformat/core": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz", + "integrity": "sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==", + "license": "MIT", + "dependencies": { + "@messageformat/date-skeleton": "^1.0.0", + "@messageformat/number-skeleton": "^1.0.0", + "@messageformat/parser": "^5.1.0", + "@messageformat/runtime": "^3.0.1", + "make-plural": "^7.0.0", + "safe-identifier": "^0.4.1" + } + }, + "node_modules/@messageformat/date-skeleton": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz", + "integrity": "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==", + "license": "MIT" + }, + "node_modules/@messageformat/number-skeleton": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz", + "integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==", + "license": "MIT" + }, + "node_modules/@messageformat/parser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.1.tgz", + "integrity": "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==", + "license": "MIT", + "dependencies": { + "moo": "^0.5.1" + } + }, + "node_modules/@messageformat/runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.1.tgz", + "integrity": "sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==", + "license": "MIT", + "dependencies": { + "make-plural": "^7.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3727,9 +3772,9 @@ "license": "CC0-1.0" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4454,6 +4499,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-printf": { + "version": "1.6.10", + "resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.10.tgz", + "integrity": "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=10.0" + } + }, "node_modules/fast-xml-parser": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", @@ -5101,6 +5155,26 @@ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, + "node_modules/i18n": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/i18n/-/i18n-0.15.2.tgz", + "integrity": "sha512-mdBxCfC651UL/hNizIQgB1NHwbBKjlrPcsoTzd/X8rNbJlS1FMF//TOyHEVFg9Dxo0RcrI2ZKt1AFTNe3Q40og==", + "license": "MIT", + "dependencies": { + "@messageformat/core": "^3.4.0", + "debug": "^4.4.3", + "fast-printf": "^1.6.10", + "make-plural": "^7.4.0", + "math-interval-parser": "^2.0.1", + "mustache": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/mashpie" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5702,6 +5776,12 @@ "source-map-js": "^1.2.0" } }, + "node_modules/make-plural": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.4.0.tgz", + "integrity": "sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==", + "license": "Unicode-DFS-2016" + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -5712,6 +5792,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/math-interval-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-2.0.1.tgz", + "integrity": "sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6648,6 +6737,12 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "license": "BSD-3-Clause" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -6663,6 +6758,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -7598,6 +7702,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", + "license": "ISC" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", diff --git a/package.json b/package.json index 54e3c7a..49088bc 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "alpinejs": "^3.14.9", "astro": "^5.6.1", "astro-icon": "^1.1.5", + "i18n": "^0.15.2", "preact": "^10.26.2", "prismjs": "^1.30.0", "tailwindcss": "^4.0.8" diff --git a/src/components/blog/DatePub.astro b/src/components/blog/DatePub.astro index 15f79db..c8998db 100644 --- a/src/components/blog/DatePub.astro +++ b/src/components/blog/DatePub.astro @@ -1,5 +1,10 @@ --- +import { getLangFromUrl, useTranslations } from "../../i18n/utils"; +const lang = getLangFromUrl(Astro.url); + const {date, class: className} = Astro.props; + +let localeDate = lang === 'fr' ? 'fr-FR' : 'en-US'; --- { - new Date(date).toLocaleDateString("en-US", { + new Date(date).toLocaleDateString(localeDate, { timeZone: "UTC", // Avoid timezone adjustments day: "numeric", month: "long", diff --git a/src/components/blog/Hero.astro b/src/components/blog/Hero.astro index 3fe4fc7..ffa1173 100644 --- a/src/components/blog/Hero.astro +++ b/src/components/blog/Hero.astro @@ -6,6 +6,10 @@ import { Icon } from "astro-icon/components"; import Heading from "../ui/Heading.astro"; import { AstroError } from "astro/errors"; import { getCollection} from "astro:content"; +import { getLangFromUrl, useTranslations } from "../../i18n/utils"; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); const [staticData] = await getCollection('staticData'); @@ -42,7 +46,7 @@ if (!staticData) { >
@@ -68,11 +72,11 @@ if (!staticData) { class="group-hover:scale-103 ease-in-out duration-500 w-5/12 max-sm:w-auto flex justify-center items-center" >
- +
@@ -80,7 +84,7 @@ if (!staticData) { class="font-extrabold text-lg max-xl:text-base max-lg:text-sm max-lg:flex max-lg:flex-col-reverse max-md:flex-row leading-normal max-sm:leading-none" >{staticData.data.profileTitle}{t('staticData.data.profileTitle')} 🚀
diff --git a/src/components/portfolio/Experience.astro b/src/components/portfolio/Experience.astro index b0c8ff9..fb0b026 100644 --- a/src/components/portfolio/Experience.astro +++ b/src/components/portfolio/Experience.astro @@ -1,6 +1,11 @@ --- import ExperienceItem from "./ExperienceItem.astro"; -const EXPERIENCE = [ +import { getLangFromUrl, useTranslations } from "../../i18n/utils"; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); + +let EXPERIENCE = [ { date: "October 2024 - Present", title: "DevOps Engineering Student", @@ -15,8 +20,27 @@ const EXPERIENCE = [ description: "I worked as a commercial advisor and managed the computer park at Virtual Xperience, a company specializing in virtual reality experiences. My role involved assisting customers, managing user experience, and ensuring the smooth operation of the computer systems.", }, - ]; + +if(lang == 'fr') { + EXPERIENCE = [ + { + date: "Octobre 2024 - PrĂ©sent", + title: "Apprenti IngĂ©nieur DevOps", + company: "CIRAD / Polytech Montpellier", + description: + "En tant qu'apprenti IngĂ©nieur DevOps, je suis impliquĂ© dans la conception, la crĂ©ation et le dĂ©ploiement de solutions pour les chercheurs du CIRAD de Montpellier. J'Ă©tudie en parallĂšle Ă  Polytech Montpellier, oĂč j'apprends Ă  appliquer les principes DevOps pour amĂ©liorer le dĂ©veloppement d'applications et leur dĂ©ploiement.", + }, + { + date: "AoĂ»t 2023", + title: "Conseiller commercial et gestionnaire du parc informatique", + company: "Virtual Xperience", + description: + "J'ai travaillĂ© en tant que conseiller commercial et ai gĂ©rĂ© le parc informatique chez Virtual Xperience, une entreprise spĂ©cialisĂ©e dans les expĂ©riences de rĂ©alitĂ© virtuelle. Mon rĂŽle consistait Ă  assister les clients, gĂ©rer l'expĂ©rience utilisateur et assurer le bon fonctionnement des systĂšmes informatiques.", + }, + + ]; +} ---
diff --git a/src/components/portfolio/HeroIndex.astro b/src/components/portfolio/HeroIndex.astro index c72be4a..f53c542 100644 --- a/src/components/portfolio/HeroIndex.astro +++ b/src/components/portfolio/HeroIndex.astro @@ -5,7 +5,11 @@ import Tools from "../portfolio/Tools.astro"; import { Icon } from "astro-icon/components"; import Hobbies from "../portfolio/Hobbies.astro"; import { AstroError } from "astro/errors"; -import { getCollection} from "astro:content"; +import { getCollection } from "astro:content"; +import { getLangFromUrl, useTranslations } from "../../i18n/utils"; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); const [staticData] = await getCollection('staticData'); @@ -17,7 +21,7 @@ if (!staticData) {
+ aria-label={t('heroindex.aboutme')}>
@@ -57,35 +61,35 @@ if (!staticData) { - Available for work + {t('heroindex.availableForWork')}

- {staticData.data.profileName} 👋 + {t('staticData.data.profileName')} 👋

{staticData.data.profileTitle} with {t('staticData.data.profileTitle')} {t('heroindex.introduction.with')} 3 years of experience, passionate about devops, new technologies, and coffee that sparks ideas. Let's try to create something unbelievable ! 🚀✹ + >{t('heroindex.introduction.profileExperience')}{t('heroindex.introduction.end')}

diff --git a/src/components/ui/Capsule.astro b/src/components/ui/Capsule.astro index 1e8656b..9c66ce6 100644 --- a/src/components/ui/Capsule.astro +++ b/src/components/ui/Capsule.astro @@ -1,6 +1,10 @@ --- import { Icon } from "astro-icon/components"; import { getLanguage } from "../../utils/languages"; +import { getLangFromUrl, useTranslations } from "../../i18n/utils"; + +const siteLang = getLangFromUrl(Astro.url); +const t = useTranslations(siteLang); interface Props { lang: string; @@ -49,8 +53,8 @@ const getIconContainerClasses = () => { linkEnabled ? ( + {Object.entries(languages).map(([langCode, label]) => ( + + ))} + + + \ No newline at end of file diff --git a/src/components/ui/ReadMore.astro b/src/components/ui/ReadMore.astro index c0f0ef2..582a6ac 100644 --- a/src/components/ui/ReadMore.astro +++ b/src/components/ui/ReadMore.astro @@ -1,10 +1,14 @@ --- const {class: className} = Astro.props; import { Icon } from "astro-icon/components"; +import { getLangFromUrl, useTranslations } from "../../i18n/utils"; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); ---
- Read more + {t('readMore')}
diff --git a/src/components/ui/Share.astro b/src/components/ui/Share.astro index 4d4d343..91c3c3e 100644 --- a/src/components/ui/Share.astro +++ b/src/components/ui/Share.astro @@ -1,5 +1,9 @@ --- import { Icon } from "astro-icon/components"; +import { getLangFromUrl, useTranslations } from "../../i18n/utils"; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); const { tweetText, currentUrl } = Astro.props; @@ -22,8 +26,8 @@ const shareLinks = [ ]; --- -
- Share +
+ {t('share')} { shareLinks.map((link) => ( diff --git a/src/components/ui/Tag.astro b/src/components/ui/Tag.astro index 24335f0..6f3e340 100644 --- a/src/components/ui/Tag.astro +++ b/src/components/ui/Tag.astro @@ -1,9 +1,13 @@ --- const { tag, forceDark = false } = Astro.props; +import { getLangFromUrl, useTranslations } from "../../i18n/utils"; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); --- {tag} diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts new file mode 100644 index 0000000..9a7f3e7 --- /dev/null +++ b/src/i18n/ui.ts @@ -0,0 +1,149 @@ +import { getCollection } from "astro:content"; +import { AstroError } from "astro/errors"; + +const [staticData] = await getCollection('staticData'); + +if (!staticData) { + throw new AstroError("JSON data not found"); +} + +export const languages = { + en: '🇬🇧​', + fr: 'đŸ‡«đŸ‡·â€‹', +}; + +export const defaultLang = 'en'; + +export const ui = { + en: { + 'nav.home': 'Home', + 'nav.about': 'About', + 'nav.twitter': 'Twitter', + 'blog.hero.aboutMeButton': 'About Me', + 'blog.lastPost.languages': 'Programming languages', + 'blog.lastPost.readArticle': 'Read article', + 'blog.lastPost.tags': 'Article tags', + 'blog.listPosts.morePosts': 'More posts', + 'blog.listPosts.all': 'All', + 'blog.listPosts.posts': 'Posts', + 'footer.developedBy': `Developed by ${staticData.data.alias} with Astro`, + 'footer.contact.sendEmail': `Send email to ${staticData.data.email}`, + 'footer.contact.visitInstagram': `Visit ${staticData.data.alias} on Instagram`, + 'footer.contact.visitYouTube': `Visit ${staticData.data.alias} on YouTube`, + 'footer.contact.visitGitHub': `Visit ${staticData.data.alias} on GitHub`, + 'footer.contact.visitLinkedIn': `Visit ${staticData.data.alias} on LinkedIn`, + 'header.nav.toggleMenu': 'Toggle menu', + 'header.nav.home': 'Home', + 'article.nav.tableOfContents': 'Table of Contents', + 'nav.blog': 'Blog', + 'nav.blog.allPosts': 'All Posts', + 'nav.experience': 'Experience', + 'nav.projects': 'Projects', + 'nav.main': 'Main Navigation', + 'nav.socialLinks': 'Social Media Links', + 'staticData.data.githubText': 'Want to see my code ?', + 'staticData.data.profileAlt': 'Profile picture of Pierre-Louis Leclerc (Proxyfil)', + 'staticData.data.profileTitle': 'Profile of Pierre-Louis Leclerc (Proxyfil)', + 'staticData.data.alias': staticData.data.alias, + 'staticData.data.contactSectionSubtitle.work': 'Let\'s work', + 'staticData.data.contactSectionSubtitle.together': 'together.', + 'staticData.data.contactSectionButtonText': 'Contact Me', + 'staticData.data.profileName': staticData.data.profileName, + 'heroindex.sectionAriaLabel': 'Professional profile and introduction', + 'heroindex.aboutme': 'View about me page', + 'heroindex.availableForWork': 'Available for work', + 'heroindex.introduction.profileExperience': '3 years of experience', + 'heroindex.introduction.with': 'with', + 'heroindex.introduction.end': ', passionate about devops, new technologies, and coffee that sparks ideas. Let\'s try to create something unbelievable ! 🚀✹', + 'heroindex.button.aboutme': 'About Me', + 'heroindex.viewMyGitHubProfile': 'View this site\'s code repository on GitHub', + 'heroindex.seeMyWork': 'Want to see my work ?', + 'heroindex.viewProjectsSection': 'View projects section', + 'heroindex.project.introduction.1': 'I love', + 'heroindex.project.introduction.2': 'turning ideas into real projects.', + 'heroindex.project.introduction.3': 'Here I show you some of the', + 'heroindex.project.introduction.4': 'developments', + 'heroindex.project.introduction.5': 'I\'ve worked on, applying technology, design, and lots of creativity.', + 'heroindex.project.introduction.6': 'Check them out !', + 'heroindex.project.title': 'Projects', + 'heroindex.beyondTheCode': 'Beyond the Code', + 'listProjects.viewMoreButton': 'View More Projects...', + 'readMore': 'Read more', + 'share': 'Share', + 'share.ariaLabel': 'Share on social media', + 'share.shareOn': 'Share on', + 'capsule.viewPostsAbout': 'View posts about', + 'about.profileTitle': 'DevOps Engineer Apprentice', + 'about.title': 'About', + 'about.titleGradient': 'me', + 'about.experience.experience': 'Experience', + 'about.experience.experienceGradient': 'Work', + 'about.stack.title': 'Stack', + 'about.stack.titleGradient': 'Technological' + }, + fr: { + 'nav.home': 'Accueil', + 'nav.about': 'À propos', + 'blog.hero.aboutMeButton': 'À propos de moi', + 'blog.lastPost.languages': 'Langages de programmation', + 'blog.lastPost.readArticle': 'Lire l\'article', + 'blog.lastPost.tags': 'Tags de l\'article', + 'blog.listPosts.morePosts': 'Plus d\'articles', + 'blog.listPosts.all': 'Tous les', + 'blog.listPosts.posts': 'Posts', + 'footer.developedBy': `DĂ©veloppĂ© par ${staticData.data.alias} avec Astro`, + 'footer.contact.sendEmail': `Envoyer un email Ă  ${staticData.data.email}`, + 'footer.contact.visitInstagram': `Regardez ${staticData.data.alias} sur Instagram`, + 'footer.contact.visitYouTube': `Regardez ${staticData.data.alias} sur YouTube`, + 'footer.contact.visitGitHub': `Regardez ${staticData.data.alias} sur GitHub`, + 'footer.contact.visitLinkedIn': `Regardez ${staticData.data.alias} sur LinkedIn`, + 'header.nav.toggleMenu': 'Basculer le menu', + 'header.nav.home': 'Accueil', + 'article.nav.tableOfContents': 'Table des matiĂšres', + 'nav.blog': 'Blog', + 'nav.blog.allPosts': 'Tous les articles', + 'nav.experience': 'ExpĂ©rience', + 'nav.projects': 'Projets', + 'nav.main': 'Navigation principale', + 'nav.socialLinks': 'Liens vers les rĂ©seaux sociaux', + 'staticData.data.githubText': 'Envie de voir mon code ?', + 'staticData.data.profileAlt': 'Photo de profil de Pierre-Louis Leclerc (Proxyfil)', + 'staticData.data.profileTitle': 'Profil de Pierre-Louis Leclerc (Proxyfil)', + 'staticData.data.alias': staticData.data.alias, + 'staticData.data.contactSectionSubtitle.work': 'Travaillons', + 'staticData.data.contactSectionSubtitle.together': 'ensemble.', + 'staticData.data.contactSectionButtonText': 'Contactez-moi', + 'staticData.data.profileName': staticData.data.profileName, + 'heroindex.sectionAriaLabel': 'Profil professionnel et introduction', + 'heroindex.aboutme': 'Voir la page Ă  propos de moi', + 'heroindex.availableForWork': 'Disponible pour des missions', + 'heroindex.introduction.profileExperience': '3 ans d\'expĂ©rience', + 'heroindex.introduction.with': 'avec', + 'heroindex.introduction.end': ', passionnĂ© par le devops, les nouvelles technologies, et le cafĂ© qui stimule les idĂ©es. Essayons de crĂ©er quelque chose d\'incroyable ! 🚀✹', + 'heroindex.button.aboutme': 'À propos de moi', + 'heroindex.viewMyGitHubProfile': 'Voir le dĂ©pĂŽt de code de ce site sur GitHub', + 'heroindex.seeMyWork': 'Envie de voir mon travail ?', + 'heroindex.viewProjectsSection': 'Voir la section projets', + 'heroindex.project.introduction.1': 'J\'adore', + 'heroindex.project.introduction.2': 'transformer des idĂ©es en projets rĂ©els.', + 'heroindex.project.introduction.3': 'Ici, je vous montre quelques-uns des', + 'heroindex.project.introduction.4': 'projets', + 'heroindex.project.introduction.5': 'sur lesquels j\'ai travaillĂ©, en appliquant des technologies, du design, et beaucoup de crĂ©ativitĂ©.', + 'heroindex.project.introduction.6': 'DĂ©couvrez-les !', + 'heroindex.project.title': 'Projets', + 'heroindex.beyondTheCode': 'Au-delĂ  du code', + 'listProjects.viewMoreButton': 'Voir plus de projets...', + 'readMore': 'Lire la suite', + 'share': 'Partager', + 'share.ariaLabel': 'Partager sur les rĂ©seaux sociaux', + 'share.shareOn': 'Partager sur', + 'capsule.viewPostsAbout': 'Voir les posts Ă  propos de', + 'about.profileTitle': 'Apprenti IngĂ©nieur DevOps', + 'about.title': 'À propos', + 'about.titleGradient': 'de moi', + 'about.experience.experience': 'ExpĂ©rience', + 'about.experience.experienceGradient': 'Professionnelle', + 'about.stack.title': 'Stack', + 'about.stack.titleGradient': 'Technologique' + }, +} as const; \ No newline at end of file diff --git a/src/i18n/utils.ts b/src/i18n/utils.ts new file mode 100644 index 0000000..be7c558 --- /dev/null +++ b/src/i18n/utils.ts @@ -0,0 +1,16 @@ +import type { string } from 'astro:schema'; +import { ui, defaultLang } from './ui'; + +export function getLangFromUrl(url: URL) { + const [, lang]: string[] = url.pathname.split('/'); + if (lang in ui) return lang as keyof typeof ui; + return defaultLang; +} + +export function useTranslations(lang: keyof typeof ui) { + return function t(key: keyof typeof ui[typeof defaultLang]) { + const translations = ui[lang] as Record; + const fallback = ui[defaultLang] as Record; + return translations[key as string] ?? fallback[key as string]; + } +} \ No newline at end of file diff --git a/src/layouts/MarkdownAbout.astro b/src/layouts/MarkdownAbout.astro index 5060ccd..9b6fed2 100644 --- a/src/layouts/MarkdownAbout.astro +++ b/src/layouts/MarkdownAbout.astro @@ -7,6 +7,11 @@ import NavArticle from "../components/layout/NavArticle.astro"; import Contact from "../components/portfolio/Contact.astro"; import Tools from "../components/portfolio/Tools.astro"; import { getCollection} from "astro:content"; +import { getLangFromUrl, useTranslations } from "../i18n/utils"; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); + const { frontmatter } = Astro.props; const [staticData] = await getCollection('staticData'); --- @@ -50,7 +55,7 @@ const [staticData] = await getCollection('staticData'); aria-label="Photo of Pierre-Louis Leclerc (Proxyfil) for the blog" >
@@ -68,18 +73,17 @@ const [staticData] = await getCollection('staticData'); > {staticData.data.profileTitle} with {t('about.profileTitle')} {t('heroindex.introduction.with')} 3 years of experience, passionate about devops, new technologies, and coffee that - sparks ideas. Let's try to create something unbelievable ! 🚀✹ + >{t('heroindex.introduction.profileExperience')}{t('heroindex.introduction.end')}

- +
@@ -92,7 +96,7 @@ const [staticData] = await getCollection('staticData');
- +
@@ -102,7 +106,7 @@ const [staticData] = await getCollection('staticData');
- +
diff --git a/src/pages/about-me.md b/src/pages/en/about-me.md similarity index 100% rename from src/pages/about-me.md rename to src/pages/en/about-me.md diff --git a/src/pages/blog/index.astro b/src/pages/en/blog/index.astro similarity index 68% rename from src/pages/blog/index.astro rename to src/pages/en/blog/index.astro index e40ecce..9e6e310 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/en/blog/index.astro @@ -1,8 +1,8 @@ --- -import Layout from "../../layouts/Layout.astro"; -import Hero from "../../components/blog/Hero.astro"; -import Tags from "../../components/blog/Tags.astro"; -import ListPosts from "../../components/blog/ListPosts.astro"; +import Layout from "../../../layouts/Layout.astro"; +import Hero from "../../../components/blog/Hero.astro"; +import Tags from "../../../components/blog/Tags.astro"; +import ListPosts from "../../../components/blog/ListPosts.astro"; const pageTitle = "Blog - Pierre-Louis Leclerc | Proxyfil"; const description = "Everything I want to share as a blog."; const ogimage ={ diff --git a/src/pages/blog/posts/create-deb-package.md b/src/pages/en/blog/posts/create-deb-package.md similarity index 100% rename from src/pages/blog/posts/create-deb-package.md rename to src/pages/en/blog/posts/create-deb-package.md diff --git a/src/pages/blog/posts/index.astro b/src/pages/en/blog/posts/index.astro similarity index 79% rename from src/pages/blog/posts/index.astro rename to src/pages/en/blog/posts/index.astro index d73ae37..a1a7410 100644 --- a/src/pages/blog/posts/index.astro +++ b/src/pages/en/blog/posts/index.astro @@ -1,8 +1,8 @@ --- -import Layout from "../../../layouts/Layout.astro"; -import Hero from "../../../components/blog/Hero.astro"; -import Tags from "../../../components/blog/Tags.astro"; -import ListPosts from "../../../components/blog/ListPosts.astro"; +import Layout from "../../../../layouts/Layout.astro"; +import Hero from "../../../../components/blog/Hero.astro"; +import Tags from "../../../../components/blog/Tags.astro"; +import ListPosts from "../../../../components/blog/ListPosts.astro"; const pageTitle = "Blog - Pierre-Louis Leclerc | Proxyfil"; const description = "Everything I want to share as a blog."; const ogimage ={ @@ -11,10 +11,10 @@ const ogimage ={ }; const currentUrl = `${Astro.site}${Astro.url.pathname}`; const tweetText = encodeURIComponent(`"${pageTitle}"`); -import Heading from "../../../components/ui/Heading.astro"; -import Share from "../../../components/ui/Share.astro"; +import Heading from "../../../../components/ui/Heading.astro"; +import Share from "../../../../components/ui/Share.astro"; -import Languages from "../../../components/blog/Languages.astro"; +import Languages from "../../../../components/blog/Languages.astro"; --- diff --git a/src/pages/blog/posts/retex-sfh.md b/src/pages/en/blog/posts/retex-sfh.md similarity index 100% rename from src/pages/blog/posts/retex-sfh.md rename to src/pages/en/blog/posts/retex-sfh.md diff --git a/src/pages/blog/posts/retex-zevent2025.md b/src/pages/en/blog/posts/retex-zevent2025.md similarity index 100% rename from src/pages/blog/posts/retex-zevent2025.md rename to src/pages/en/blog/posts/retex-zevent2025.md diff --git a/src/pages/blog/tags/[tag].astro b/src/pages/en/blog/tags/[tag].astro similarity index 85% rename from src/pages/blog/tags/[tag].astro rename to src/pages/en/blog/tags/[tag].astro index 9f61377..40db3db 100644 --- a/src/pages/blog/tags/[tag].astro +++ b/src/pages/en/blog/tags/[tag].astro @@ -1,7 +1,7 @@ --- -import Layout from '../../../layouts/Layout.astro'; -import BlogPost from '../../../components/blog/BlogPost.astro'; -import Heading from '../../../components/ui/Heading.astro'; +import Layout from '../../../../layouts/Layout.astro'; +import BlogPost from '../../../../components/blog/BlogPost.astro'; +import Heading from '../../../../components/ui/Heading.astro'; export async function getStaticPaths() { const allPosts: any[] = await Astro.glob("../posts/*.md"); diff --git a/src/pages/blog/tags/index.astro b/src/pages/en/blog/tags/index.astro similarity index 58% rename from src/pages/blog/tags/index.astro rename to src/pages/en/blog/tags/index.astro index 93b0243..29789f0 100644 --- a/src/pages/blog/tags/index.astro +++ b/src/pages/en/blog/tags/index.astro @@ -1,7 +1,7 @@ --- -import Layout from "../../../layouts/Layout.astro"; -import Tags from "../../../components/blog/Tags.astro"; -import Heading from "../../../components/ui/Heading.astro"; +import Layout from "../../../../layouts/Layout.astro"; +import Tags from "../../../../components/blog/Tags.astro"; +import Heading from "../../../../components/ui/Heading.astro"; const pageTitle = "Tags"; diff --git a/src/pages/blog/techs/[category].astro b/src/pages/en/blog/techs/[category].astro similarity index 88% rename from src/pages/blog/techs/[category].astro rename to src/pages/en/blog/techs/[category].astro index a721b29..3a8227a 100644 --- a/src/pages/blog/techs/[category].astro +++ b/src/pages/en/blog/techs/[category].astro @@ -1,7 +1,7 @@ --- -import Layout from "../../../layouts/Layout.astro"; -import BlogPost from "../../../components/blog/BlogPost.astro"; -import Heading from "../../../components/ui/Heading.astro"; +import Layout from "../../../../layouts/Layout.astro"; +import BlogPost from "../../../../components/blog/BlogPost.astro"; +import Heading from "../../../../components/ui/Heading.astro"; import type { MarkdownInstance } from 'astro'; interface Frontmatter { diff --git a/src/pages/blog/techs/index.astro b/src/pages/en/blog/techs/index.astro similarity index 75% rename from src/pages/blog/techs/index.astro rename to src/pages/en/blog/techs/index.astro index 290f00a..46adad9 100644 --- a/src/pages/blog/techs/index.astro +++ b/src/pages/en/blog/techs/index.astro @@ -1,7 +1,7 @@ --- -import Layout from "../../../layouts/Layout.astro"; -import Languages from "../../../components/blog/Languages.astro"; -import Heading from "../../../components/ui/Heading.astro"; +import Layout from "../../../../layouts/Layout.astro"; +import Languages from "../../../../components/blog/Languages.astro"; +import Heading from "../../../../components/ui/Heading.astro"; const pageTitle = "Technologies"; // Fixed: Title was "Languages" but heading shows "Technologies" const allPosts = await Astro.glob("../posts/*.md"); diff --git a/src/pages/en/index.astro b/src/pages/en/index.astro new file mode 100644 index 0000000..f32bb65 --- /dev/null +++ b/src/pages/en/index.astro @@ -0,0 +1,66 @@ +--- +import Layout from "../../layouts/Layout.astro"; +import Experience from "../../components/portfolio/Experience.astro"; +import HeroIndex from "../../components/portfolio/HeroIndex.astro"; +import Contact from "../../components/portfolio/Contact.astro"; +import ListProjects from "../../components/portfolio/ListProjects.astro"; +import { Icon } from "astro-icon/components"; +const pageTitle = "Portfolio - Pierre-Louis Leclerc | Proxyfil"; +const description = "Everything you need to know about my portfolio as a DevOps engineer student. Discover my experience, projects, and skills in development and technology."; +const ogimage = { + url: "/images/imagedefault.webp", + alt: "Screenshot of a web portfolio with a modern and dark design. Featuring Pierre-Louis Leclerc, a DevOps engineer student with 3 years of experience, highlighting his passion for development and technology. Includes contact sections, projects, and a technology stack with technologies like HTML5, JavaScript, TypeScript, Angular, Node.js, CSS, Tailwind, and more.", +}; +import Heading from "../../components/ui/Heading.astro"; +--- + + + + +
+
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+

+ I love turning ideas into real projects. +
Here I show you some of the developments I've worked on, applying technology, design, and lots of creativity. + Check them out! +

+
+
+
+ + + +
diff --git a/src/pages/portfolio/projects/ingdoc.md b/src/pages/en/portfolio/projects/ingdoc.md similarity index 100% rename from src/pages/portfolio/projects/ingdoc.md rename to src/pages/en/portfolio/projects/ingdoc.md diff --git a/src/pages/portfolio/projects/pollpy.md b/src/pages/en/portfolio/projects/pollpy.md similarity index 100% rename from src/pages/portfolio/projects/pollpy.md rename to src/pages/en/portfolio/projects/pollpy.md diff --git a/src/pages/portfolio/projects/tungstene_enriched.md b/src/pages/en/portfolio/projects/tungstene_enriched.md similarity index 100% rename from src/pages/portfolio/projects/tungstene_enriched.md rename to src/pages/en/portfolio/projects/tungstene_enriched.md diff --git a/src/pages/robots.txt.ts b/src/pages/en/robots.txt.ts similarity index 100% rename from src/pages/robots.txt.ts rename to src/pages/en/robots.txt.ts diff --git a/src/pages/rss.xml.js b/src/pages/en/rss.xml.js similarity index 100% rename from src/pages/rss.xml.js rename to src/pages/en/rss.xml.js diff --git a/src/pages/fr/about-me.md b/src/pages/fr/about-me.md new file mode 100644 index 0000000..5e718a4 --- /dev/null +++ b/src/pages/fr/about-me.md @@ -0,0 +1,50 @@ +--- +layout: /src/layouts/MarkdownAbout.astro +title: "A propos - Pierre-Louis Leclerc | Proxyfil" +description: "Étudiant en DevOps et passionnĂ© de donnĂ©es. De mes dĂ©buts en dĂ©veloppement Ă  la crĂ©ation de communautĂ©s et de projets impactants, ici je partage mes erreurs, expĂ©riences et apprentissages. 🚀⭐" +author: "Pierre-Louis Leclerc" +image: + url: "/images/proxyfil.webp" + alt: "Photo de Pierre-Louis Leclerc (Proxyfil) pour le blog" +--- + +## Comment j'ai commencĂ© Ă  apprendre le DevOps 🚀 + +J'ai commencĂ© Ă  crĂ©er mes premiers projets sĂ©rieux en 2021. Autant que je me souvienne, j'ai toujours Ă©tĂ© hypnotisĂ© par les ordinateurs et leur magie. J'ai appris Ă  utiliser Internet trĂšs tĂŽt et j'ai perdu des tonnes d'heures sur **Minecraft** ou **Factorio** depuis l'Ăąge de 12 ans. Les annĂ©es ont passĂ© et j'ai dĂ©couvert les premiers mods ainsi que la programmation avec certains jeux auxquels j'ai jouĂ© pendant des annĂ©es. Ce type primitif de programmation avec Lua et des fichiers yaml a suscitĂ© une certaine curiositĂ© dans mon esprit quant Ă  la crĂ©ation de choses avec du code. + +Quand j'Ă©tais au collĂšge, j'ai appris HTML/CSS et j'ai commencĂ© Ă  crĂ©er de petits sites web. J'ai aussi achetĂ© un livre sur les bases du C et j'Ă©tais terrible Ă  ça parce que c'Ă©tait beaucoup trop difficile pour moi, mais je m'amusais bien avec. + +## Premiers pas sĂ©rieux đŸ–„ïž + +En 2019, je suis allĂ© dans un lycĂ©e avec une classe d'informatique et d'ingĂ©nierie. J'ai commencĂ© Ă  apprendre les bases de l'informatique avec mes premiers langages comme **Python** et **JavaScript**. +Puis les annĂ©es ont passĂ© et en projet j'ai créé des sites web entiers comme des forums en **PHP** et mes premiers bots Twitch pour faire de l'analyse de donnĂ©es. Avec **Python**, j'ai appris la POO et créé mon premier jeu : un petit "**puissance 4**". +Pour ma 2Ăšme annĂ©e de lycĂ©e, j'ai Ă©galement créé un petit jeu textuel avec des choix sur ma calculatrice. C'Ă©tait plutĂŽt amusant mĂȘme si ce n'Ă©tait pas vraiment compliquĂ©. + +## Explorer Twitch et les communautĂ©s đŸ€– + +En 2022, j'ai dĂ» crĂ©er mon premier gros projet pour mes cours d'informatique. J'ai choisi de crĂ©er un outil pour rĂ©cupĂ©rer des donnĂ©es de Twitch et en faire des statistiques et des analyses. Il a d'abord Ă©tĂ© construit en **Python** avec des **fichiers JSON**, mais j'ai rapidement changĂ© pour **NodeJS** et une base de donnĂ©es **Postgresql**. +Aujourd'hui, ce projet appelĂ© "Tungstene Enriched" fonctionne encore de temps en temps quand je veux l'utiliser. En 2022, c'Ă©tait vraiment petit, mais de nos jours, c'est beaucoup plus gros et j'ai ajoutĂ© des fonctionnalitĂ©s au fil des ans, comme la surveillance des donnĂ©es avec **Grafana** ou des processus et publications automatiques. + +La derniĂšre fois que ce projet a Ă©tĂ© utilisĂ©, c'Ă©tait ici : zlan2025.gdoc.fr +Si vous voulez plus d'informations sur ce projet, consultez ce fil : Fil "Comment (ne pas) monter une infra" + +## Apprenons le DevOps 💡 + +En 2024, j'ai dĂ©mĂ©nagĂ© de ma petite ville Ă  Montpellier en France. LĂ , j'ai rejoint une **Ă©cole d'ingĂ©nierie** pour apprendre le DevOps et augmenter mes connaissances globales en informatique. +AprĂšs quelques mois, j'ai acquis beaucoup plus de connaissances que dans toute ma vie, explorĂ© de nombreuses technologies et paradigmes. Je pense Ă©galement que l'apprentissage Ă  temps partiel au sein du CIRAD, qui est une entreprise travaillant sur des sujets **Ă©cologiques** et **d'agriculture durable**, m'a beaucoup aidĂ©. +Je suis chargĂ© de crĂ©er de nouveaux outils pour les scientifiques ou de maintenir ceux existants afin d'assurer la bonne santĂ© des outils informatiques internes. + +## Partager mes expĂ©riences 🧠 + +Partager des connaissances et des leçons est quelque chose de vraiment important pour moi. Lorsque j'ai commencĂ© Ă  apprendre Ă  coder, j'ai rencontrĂ© de nombreux Ă©checs et dĂ©ceptions, mais ces obstacles m'ont enrichi d'une certaine maniĂšre. Donc, lorsque je fais quelque chose dont j'ai appris quelque chose, j'essaie de **le partager** sur Internet pour obtenir des retours. +Le blog ici est un exemple de cela, mais je publie Ă©galement sur mon **Twitter** et j'ai Ă©galement donnĂ© une petite confĂ©rence lors de Polycloud 2025 pour parler de Twitch et des outils que je dĂ©veloppe. Ce fut une bonne expĂ©rience ! + +> Lorsque vous partagez, vous continuez Ă  apprendre, alors partageons les connaissances partout ! + +## What's Next... 🚀 + +Depuis 2020, je fais partie du groupe communautaire **InGDoc**. Avec eux, je crĂ©e des projets innovants et crĂ©atifs autour des donnĂ©es et d'Internet, puisque l'Internet ne mourra probablement jamais, vous pouvez vous attendre Ă  ce que je crĂ©e beaucoup de choses autour de cela dans les mois Ă  venir. Il me reste encore 2 ans avant d'obtenir mon diplĂŽme, c'est mon prochain grand objectif, alors rendez-vous en 2027 ! + +Je suis **Pierre-Louis Leclerc**, et je vous remercie d'avoir lu cela. + +## CrĂ©ons des choses incroyables ! 🚀 \ No newline at end of file diff --git a/src/pages/fr/blog/index.astro b/src/pages/fr/blog/index.astro new file mode 100644 index 0000000..cc89bba --- /dev/null +++ b/src/pages/fr/blog/index.astro @@ -0,0 +1,22 @@ +--- +import Layout from "../../../layouts/Layout.astro"; +import Hero from "../../../components/blog/Hero.astro"; +import Tags from "../../../components/blog/Tags.astro"; +import ListPosts from "../../../components/blog/ListPosts.astro"; +const pageTitle = "Blog - Pierre-Louis Leclerc | Proxyfil"; +const description = "Tout ce que je veux partager sous forme de blog."; +const ogimage ={ + url: "/images/blogimage.webp", + alt: "Logo proxyfil.fr avec un fond dorĂ© et un effet de lumiĂšre. Texte: 'Lets learn tech !' et URL 'www.proxyfil.fr'." +}; +--- + + + + + +
+ +
+ +
diff --git a/src/pages/fr/blog/posts/create-deb-package.md b/src/pages/fr/blog/posts/create-deb-package.md new file mode 100644 index 0000000..f32d914 --- /dev/null +++ b/src/pages/fr/blog/posts/create-deb-package.md @@ -0,0 +1,210 @@ +--- +layout: /src/layouts/MarkdownPostLayout.astro +title: Comment crĂ©er un paquet .deb et mettre en place un miroir pour le distribuer +author: Pierre-Louis Leclerc | Proxyfil +description: "Tout programme peut ĂȘtre transformĂ© en paquet .deb. C'est vraiment utile pour les petits scripts qu'on utilise souvent, essayons d'en crĂ©er un ensemble ! ⚙" +image: + url: "/images/posts/create-deb-package.webp" + alt: "Illustration avec le logo Linux et un paquet." +pubDate: 2025-06-02 +tags: + [ + "Bash", "System" + + ] +languages: ["bash"] +--- + +CrĂ©er un paquet .deb peut s'avĂ©rer vraiment pratique quand on a des tĂąches rĂ©pĂ©titives Ă  effectuer. C'est Ă©galement un moyen simple de partager ses outils via un miroir et de diffuser ses paquets de gestion systĂšme sur internet. +Voyons ensemble comment crĂ©er un paquet et le partager au monde entier ! + +## 📁 Comment crĂ©er votre paquet ? + +### đŸ—ïž Architecture du paquet + +--- + +N'importe quoi peut devenir un paquet .deb, c'est la principale chose Ă  retenir. +Avec ce travail, il existe des standards dans l'architecture de votre paquet, vous devriez suivre ces conventions : + +```bash +[package_name]/ +├── DEBIAN/ +│ ├── control +│ └── postinst +│ └── prerm +└── lib/[package_name]/ + └── [script_sources] +``` + +Ici, on peut remarquer plusieurs choses. Le dossier `DEBIAN` contient tous les hooks et informations gĂ©nĂ©rales de notre paquet, c'est un dossier essentiel pour le paquet. +L'autre dossier contient l'emplacement d'installation de notre paquet ainsi que nos fichiers sources. + +### 📝 Contenu du fichier control + +--- + +Le fichier control est le cƓur du paquet. Voici un exemple ci-dessous. + +```bash +Package: mypackage +Version: 1.0 +Architecture: all +Maintainer: Example +Depends: python3, python3-tz +Description: Show the date +``` + +On peut voir ici les informations sur le paquet ainsi que les dĂ©pendances et la description. +Il est Ă©galement possible d'ajouter des conflits avec des dĂ©pendances pour les supprimer du systĂšme. + +### 📝 Contenu des fichiers postinst et prerm + +--- + +Le fichier postinst est utilisĂ© pour exĂ©cuter un script aprĂšs le dĂ©ploiement des fichiers du paquet, c'est utile pour crĂ©er des liens symboliques. + +```bash +#!/bin/bash +ln -s /usr/lib/[example]/[example.py] /usr/bin/[example] +chmod +x /usr/bin/[example] +``` + +Le fichier prerm est utilisĂ© pour exĂ©cuter un script aprĂšs la suppression des fichiers. + +```bash +#!/bin/bash +rm -f /usr/bin/[example] +``` + +Si vous le souhaitez, vous pouvez utiliser plusieurs hooks diffĂ©rents mais nous n'aborderons pas ce sujet ici. + +### 🆕 CrĂ©er notre paquet + +--- + +Pour crĂ©er un paquet .deb, nous allons utiliser `dpkg-deb` comme dans la commande suivante, vous devez l'exĂ©cuter au-dessus du dossier de votre paquet. + +```bash +dpkg-deb --build [example] +``` + +Cela va crĂ©er un paquet .deb de notre code. + +### ✒ Comment signer notre paquet + +--- + +MĂȘme si ce n'est pas obligatoire, nous allons sĂ©curiser notre paquet. Pour ce faire, nous allons crĂ©er une clĂ© privĂ©e avec `gpg`. + +```bash +gpg --full-generate-key +``` + +AprĂšs avoir saisi quelques informations, vous obtiendrez votre clĂ© privĂ©e ou du moins un identifiant : **Notez l'ID quelque part** +Ensuite, nous allons signer le paquet avec gpg pour obtenir un fichier .deb.sig. + +```bash +gpg --default-key [key_id] --detach-sign [example.deb] +``` + +Enfin, nous allons rĂ©cupĂ©rer la clĂ© publique qui authentifiera la signature du paquet. + +```bash +gpg --armor --export [key_id] > pubkey.asc +``` + +FĂ©licitations, vous avez votre paquet signĂ© ! 👏 + +--- + +## đŸ—„ïž Comment crĂ©er un miroir et l'authentifier + +### đŸȘž Comment crĂ©er un miroir simple + +--- + +Pour distribuer un paquet, un simple serveur apache suffit (ou NGINX pour ceux qui prĂ©fĂšrent) + +Ensuite, nous plaçons le fichier .deb dans un dossier et nous crĂ©ons les fichiers obligatoires pour distribuer le paquet dans un miroir : + +```bash +cd /var/www/html/deb +dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz +``` + +AprĂšs avoir fait cela, nous pouvons ajouter le miroir Ă  nos sources sur notre systĂšme en modifiant le fichier +``` +/etc/apt/sources.list.d/[package].list +``` +et en y mettant + +```bash +deb [trusted=yes] http://localhost/deb ./ +``` + +Vous pouvez remplacer `http://localhost/deb` par le chemin rĂ©el de votre paquet dans le miroir. +Maintenant que c'est fait, nous pouvons mettre Ă  jour nos sources et tĂ©lĂ©charger le paquet sur notre systĂšme : + +```bash +sudo apt update +sudo apt install [example.deb] +``` + +### 🔑 Comment authentifier la release de notre paquet ? + +--- + +Pour identifier notre dĂ©pĂŽt et notre release, nous allons utiliser la clĂ© privĂ©e gĂ©nĂ©rĂ©e prĂ©cĂ©demment pour signer notre fichier Release dans le miroir. +D'abord, nous crĂ©ons un fichier Release non signĂ© : + +```bash +apt-ftparchive release . > Release +``` + +Ensuite, nous signons le fichier avec `gpg` : + +```bash +gpg --default-key [key_id] -abs -o Release.gpg Release +``` + +Nous pouvons Ă©galement signer un fichier InRelease qui est la mĂ©thode Ă  privilĂ©gier car elle est Ă  jour et plus rĂ©cente. + +```bash +gpg --default-key [key_id] --clearsign -o InRelease Release +``` + +FĂ©licitations, vous avez créé votre paquet ainsi que votre miroir et l'avez sĂ©curisĂ© ! 👏 + +### 🔍 Comment cela se passe-t-il en rĂ©alitĂ© + +Dans de nombreux cas, vous ne crĂ©erez que les fichiers `InRelease` et `Release` car c'est plus rĂ©cent et cela coĂ»te moins cher pour apt. +Vous pouvez Ă©galement crĂ©er un fichier `Release.gpg` mais c'est l'ancienne mĂ©thode et cela nĂ©cessite qu'apt fasse 2 requĂȘtes au serveur au lieu d'une. + +Signer le paquet n'est pas vraiment utilisĂ© de nos jours, c'est plutĂŽt utilisĂ© pour signer le fichier Release du miroir. + +Par exemple, nous pouvons voir ci-dessous comment un miroir est souvent structurĂ© : + +```bash +dists/ +└── stable/ + ├── Release + ├── InRelease + └── main/ + ├── binary-amd64/ + │ └── Packages + ├── binary-i386/ + │ └── Packages + └── source/ + └── Sources +``` + +Nous pouvons crĂ©er plusieurs releases pour diffĂ©rents programmes et architectures, c'est vraiment utile quand vous voulez distribuer beaucoup de paquets. +Comme nous pouvons le voir, nous n'avons qu'un seul fichier `Release` et `InRelease` pour tout le miroir, c'est parce que nous n'allons pas signer les paquets mais seulement le fichier release. +C'est la façon la plus courante de crĂ©er un miroir et de distribuer des paquets, c'est aussi la façon utilisĂ©e par les miroirs officiels Debian et Ubuntu. + +## 💚 Conclusion + +CrĂ©er un paquet peut ĂȘtre utile dans tellement de situations, c'est l'une des façons de le faire et de gĂ©rer votre systĂšme mais vous avez plein d'autres façons de le faire. + +J'espĂšre que cet article vous a Ă©tĂ© utile, Ă  bientĂŽt... \ No newline at end of file diff --git a/src/pages/fr/blog/posts/index.astro b/src/pages/fr/blog/posts/index.astro new file mode 100644 index 0000000..1a0b784 --- /dev/null +++ b/src/pages/fr/blog/posts/index.astro @@ -0,0 +1,54 @@ +--- +import Layout from "../../../../layouts/Layout.astro"; +import Hero from "../../../../components/blog/Hero.astro"; +import Tags from "../../../../components/blog/Tags.astro"; +import ListPosts from "../../../../components/blog/ListPosts.astro"; +const pageTitle = "Blog - Pierre-Louis Leclerc | Proxyfil"; +const description = "Tout ce que je veux partager sous forme de blog."; +const ogimage ={ + url: "/images/blogimage.webp", + alt: "Logo proxyfil.fr avec un fond dorĂ© et un effet de lumiĂšre. Texte: 'Lets learn tech !' et URL 'www.proxyfil.fr'." +}; +const currentUrl = `${Astro.site}${Astro.url.pathname}`; +const tweetText = encodeURIComponent(`"${pageTitle}"`); +import Heading from "../../../../components/ui/Heading.astro"; +import Share from "../../../../components/ui/Share.astro"; + +import Languages from "../../../../components/blog/Languages.astro"; +--- + + +
+
+
+ +
+ +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ +
+
+
+
+ +
diff --git a/src/pages/blog/posts/lettre.md b/src/pages/fr/blog/posts/lettre.md similarity index 100% rename from src/pages/blog/posts/lettre.md rename to src/pages/fr/blog/posts/lettre.md diff --git a/src/pages/fr/blog/posts/retex-sfh.md b/src/pages/fr/blog/posts/retex-sfh.md new file mode 100644 index 0000000..31a7984 --- /dev/null +++ b/src/pages/fr/blog/posts/retex-sfh.md @@ -0,0 +1,158 @@ +--- +layout: /src/layouts/MarkdownPostLayout.astro +title: Retour d'expĂ©rience d'incident avec Stream For Humanity +author: Pierre-Louis Leclerc | Proxyfil +description: "Tout ne se passe pas toujours comme prĂ©vu. En travaillant sur Stream For Humanity avec Tungstene, j'ai Ă©tĂ© ciblĂ© par une attaque DDOS et j'ai appris Ă  la dure comment gĂ©rer ce type d'attaque... Partageons les leçons que j'en ai tirĂ©es." +image: + url: "/images/posts/retex-sfh.webp" + alt: "Illustration de Stream For Humanity avec le titre de l'article de blog" +pubDate: 2025-01-20 +tags: + [ + "Retex", "System", "Twitch", "Network" + + ] +languages: ["vue", "kubernetes", "cloudflare"] +--- + +Parfois on a des plans mais l'univers en a d'autres pour nous... Je m'en suis rendu compte ce mois-ci car j'ai Ă©tĂ© attaquĂ© sur mon systĂšme. Pour ĂȘtre honnĂȘte, c'Ă©tait un peu de ma faute et mĂȘme si ce n'Ă©tait pas un gros problĂšme, j'ai dĂ» trouver comment le gĂ©rer. + +Plongeons dans le problĂšme ! + +## ❓ Rapport d'incident : Attaque par dĂ©ni de service distribuĂ© (DDoS) + +### 1. RĂ©sumĂ© de l'incident + +RĂ©sumons le contexte de cette attaque : + +Le **17 janvier 2025**, une attaque par dĂ©ni de service distribuĂ© (DDoS) a Ă©tĂ© dĂ©tectĂ©e sur l'infrastructure fournie par l'Ă©cole. L'attaque a commencĂ© Ă  22h30 (UTC+1) et s'est poursuivie jusqu'Ă  23h08 (UTC+1), affectant les services dĂ©ployĂ©s pour l'Ă©vĂ©nement "Stream For Humanity", ainsi que les performances de mon serveur et du service SSH dessus. Tout le trafic acheminĂ© via un proxy Cloudflare a Ă©tĂ© redirigĂ© vers le rĂ©seau DO de l'UniversitĂ© de Montpellier, ciblant le serveur "Antlia". + +Cette attaque a utilisĂ© des techniques avancĂ©es, notamment des **attaques au niveau de la couche application**, et a principalement ciblĂ© les services d'API web dĂ©ployĂ©s sur la lame serveur. Les mesures d'attĂ©nuation ont permis de restaurer les services essentiels en 20 minutes. + +--- + +### 2. Comment je l'ai dĂ©tectĂ©e et comment j'ai rĂ©pondu initialement + +L'attaque a Ă©tĂ© dĂ©tectĂ©e via les outils de surveillance du serveur et les alertes Cloudflare. Les anomalies initiales comprenaient une augmentation de l'utilisation du CPU et une hausse significative de la latence rĂ©seau. +C'Ă©tait inhabituel pour un tel service lors d'un tel Ă©vĂ©nement et mĂȘme si j'avais dĂ©ployĂ© quelque chose d'un peu nouveau sur mon infrastructure, j'ai rapidement rĂ©alisĂ© que quelque chose n'allait pas... + +Les actions suivantes ont Ă©tĂ© prises rapidement : + +- **Blocage** des adresses IP malveillantes identifiĂ©es +- Activation des **rĂšgles d'urgence** sur le pare-feu d'applications web (WAF) +- DĂ©ploiement d'**applications supplĂ©mentaires** pour absorber le trafic + +--- + +### 3. Analyse de l'attaque + +Par la suite, j'ai pris le temps d'analyser l'attaque, d'identifier le problĂšme principal et comment il a Ă©tĂ© exploitĂ© pour causer de tels dĂ©gĂąts. + +L'analyse a rĂ©vĂ©lĂ© le vecteur suivant : + +- **Attaque au niveau de la couche application :** RequĂȘtes HTTP GET massives ciblant "sfh.proxyfil.fr/api/pools/streamers" (et par extension le domaine antlia.dopolytech.fr/api/pools/streamers) + +On peut noter quelques caractĂ©ristiques sur certains aspects techniques de l'attaque : + +- **Volume total :** ~200 000 requĂȘtes par minute, environ ~3 000 requĂȘtes par seconde (RPS), avec un pic Ă  4,73 millions de requĂȘtes par minute attĂ©nuĂ©es par le WAF de Cloudflare Ă  22h52 +- **Sources de l'attaque :** Un grand nombre d'adresses IP provenant de nombreux pays, principalement l'IndonĂ©sie (7,5 millions), les USA (3,74 millions), l'Inde (1,6 million), le BrĂ©sil (1,36 million), la Turquie (1,3 million) et la Chine (volume inconnu) +- **Outils utilisĂ©s par les attaquants :** Probablement des botnets + +Une analyse gĂ©ographique a montrĂ© une concentration d'attaquants en Asie et dans les AmĂ©riques. + +**Volume total de l'attaque :** + +- **8 Go** de donnĂ©es ont Ă©tĂ© servis par le serveur pendant l'attaque, **161,92 Go** ont Ă©tĂ© servis par le cache de Cloudflare +- **5 millions de requĂȘtes** ont Ă©tĂ© servies par le serveur, **25 millions** ont Ă©tĂ© mises en cache ou bloquĂ©es par Cloudflare +- **5 pays** reprĂ©sentaient plus de la moitiĂ© des requĂȘtes traitĂ©es par le serveur +- Sur les **30 millions de requĂȘtes enregistrĂ©es** pendant l'attaque, environ **27 millions** se sont terminĂ©es sans rĂ©ponse en raison de la surcharge du serveur ou de la rĂ©ponse du WAF +- L'utilisation du CPU sur la lame serveur a atteint **90 %** pendant l'attaque, l'utilisation de la RAM a augmentĂ© de **6 Go** + +--- + +### 4. Impacts sur les services + +**Services impactĂ©s :** + +- Services sur le serveur Antlia : Performances fortement dĂ©gradĂ©es pendant plusieurs minutes +- Lame serveur Antlia : Performances fortement dĂ©gradĂ©es pendant plusieurs minutes +- Services API et Frontend : Performances fortement dĂ©gradĂ©es pendant quelques minutes + +--- + +### 5. Actions correctives immĂ©diates + +Les actions suivantes ont Ă©tĂ© prises pour restaurer les opĂ©rations : + +- DĂ©ploiement d'un WAF via Cloudflare pour bloquer les requĂȘtes illĂ©gitimes +- Augmentation de la disponibilitĂ© des services pour absorber le trafic +- Surveillance renforcĂ©e des systĂšmes pendant les heures suivantes + +--- + +### 6. Conclusion + +Cet incident met en Ă©vidence la nĂ©cessitĂ© d'une vigilance accrue face aux menaces DDoS, qui deviennent de plus en plus sophistiquĂ©es. Bien que les mesures existantes aient contribuĂ© Ă  limiter l'impact, l'amĂ©lioration continue des outils et des processus de sĂ©curitĂ© est essentielle pour garantir la disponibilitĂ© des services. + +Les leçons tirĂ©es de cet incident Ă©claireront les futures stratĂ©gies de sĂ©curitĂ©, garantissant une rĂ©ponse rapide et efficace aux futures attaques, et servant d'avertissement aux autres Ă©tudiants qui prĂ©voient de dĂ©ployer de tels services sur l'infrastructure DO. + +--- + +### 7. Chronologie de l'attaque + +**22h30 :** DĂ©but de l'attaque DDoS +**22h36 :** DĂ©connexion du VPN Polytech +**22h37 :** EnquĂȘte auprĂšs des pairs ; le site web est inaccessible +**22h39 :** Alerte d'utilisation du CPU pour le serveur Antlia +**22h40 :** VPN restaurĂ© +**22h41 :** DĂ©ploiement de 2 nouveaux services pour absorber le trafic +**22h43 :** WAF dĂ©ployĂ© pour bloquer les 3 principaux pays attaquants +**22h46 :** Blocage de 4 pays attaquants supplĂ©mentaires +**22h49 :** PremiĂšre offensive terminĂ©e +**22h51 :** DĂ©but de la deuxiĂšme offensive, entiĂšrement attĂ©nuĂ©e par le WAF de Cloudflare +**22h52 :** Pic d'attaque, 4,73 millions de requĂȘtes attĂ©nuĂ©es. Infrastructure DO/Polytech/UM Ă©pargnĂ©e +**23h01 :** Retour au service normal, confirmĂ© par les mĂ©triques du serveur Antlia +**23h08 :** Fin de l'attaque DDoS + +--- + +### 8. Annexes + +Pays d'origine des attaques (Top 5 des attaquants) : + +![country_details.webp](/images/posts/retex-sfh/country_details.webp) + +Volume de l'attaque : + +![total_traffic.webp](/images/posts/retex-sfh/total_traffic.webp) + +Statistiques de l'attaque : + +![sources_traffic.webp](/images/posts/retex-sfh/sources_traffic.webp) + +Cartographie de l'attaque : + +![map.webp](/images/posts/retex-sfh/map.webp) + +Volume de l'attaque : + +![cached_requests.webp](/images/posts/retex-sfh/cached_requests.webp) + +Volume de donnĂ©es de l'attaque : + +![cached_bandwidth.webp](/images/posts/retex-sfh/cached_bandwidth.webp) + +Utilisation du CPU pendant l'attaque : + +![cpu_usage.webp](/images/posts/retex-sfh/cpu_usage.webp) + +Utilisation de la RAM pendant l'attaque : + +![ram_usage.webp](/images/posts/retex-sfh/ram_usage.webp) + + +## 💚 Conclusion + +Cette expĂ©rience a Ă©tĂ© vraiment utile pour moi car j'ai beaucoup appris sur la sĂ©curisation des services. C'Ă©tait la premiĂšre fois que j'Ă©tais vraiment attaquĂ© et je suis sĂ»r que la prochaine fois, je m'assurerai d'avoir des protections pour me dĂ©fendre contre de telles mĂ©thodes d'attaque sournoises. + +J'espĂšre que cet article vous a Ă©tĂ© utile, Ă  bientĂŽt... \ No newline at end of file diff --git a/src/pages/fr/blog/posts/retex-zevent2025.md b/src/pages/fr/blog/posts/retex-zevent2025.md new file mode 100644 index 0000000..b445fdc --- /dev/null +++ b/src/pages/fr/blog/posts/retex-zevent2025.md @@ -0,0 +1,123 @@ +--- +layout: /src/layouts/MarkdownPostLayout.astro +title: Anti-DDOS, Rust et apprendre de ses erreurs +author: Pierre-Louis Leclerc | Proxyfil +description: "Depuis maintenant 4 ans, je collecte des statistiques autour de l'Ă©vĂ©nement caritatif 'ZEvent'. Et mĂȘme si j'ai l'habitude, il y a toujours de nombreux problĂšmes auxquels je dois faire face quand on travaille Ă  cette Ă©chelle ! Parlons un peu des problĂšmes autour des Ă©vĂ©nements caritatifs et des mauvais choix techniques." +image: + url: "/images/posts/retex-zevent2025.webp" + alt: "Illustration du ZEvent2025 avec le titre de l'article de blog" +pubDate: 2025-09-17 +tags: + [ + "Retex", "System", "Twitch", "Rust" + + ] +languages: ["vue", "kubernetes", "cloudflare"] +--- + +Depuis 2020, je fais du travail communautaire autour du ZEvent. Cet Ă©vĂ©nement est le plus grand Ă©vĂ©nement caritatif français hĂ©bergĂ© sur Twitch, avec plus de 16 000 000€ collectĂ©s en 2025, c'est la plus importante collecte de fonds avec des streamers chaque annĂ©e. + +Chaque annĂ©e, il y a plus de personnes qui essaient de participer, plus de POV Ă  suivre, plus d'objectifs de dons Ă  voir et un spectateur moyen peut rapidement ĂȘtre submergĂ© par toutes ces informations. +Depuis 5 ans maintenant, je travaille avec un groupe de personnes pour donner aux spectateurs des outils et des mĂ©triques pour mieux suivre et comprendre cet Ă©vĂ©nement en temps rĂ©el ou aprĂšs l'Ă©vĂ©nement. + +Parlons un peu de mon travail et de ce qui rend cette annĂ©e spĂ©ciale ! + +## đŸ“± Travail habituel + + +### 📜 Liste des objectifs de dons + +Depuis 2020, nous travaillons ensemble avec [l'Ă©quipe](https://gdoc.fr/team) pour donner Ă  tout le monde un endroit avec tous les objectifs de dons. +Les objectifs de dons sont des objectifs avec des montants en euros, chaque streamer a les siens pour les dons qu'il collecte. + +En 2021, il y avait 49 streamers ensemble pendant ~54 heures, collectant tous ensemble plus de 10 000 000€ pour la premiĂšre fois. +Cette annĂ©e, environ 800 objectifs de dons ont Ă©tĂ© créés et 682 atteints, bien plus qu'un spectateur moyen ne peut suivre. + + + + +### 📊 Statistiques + +Depuis 2021, nous crĂ©ons des infographies et des visuels avec de nombreuses donnĂ©es autour du ZEvent et des mĂ©triques Twitch. + +Les donnĂ©es peuvent concerner le temps de stream, le temps visionnĂ©, le nombre d'emotes ou de messages envoyĂ©s. +Tout est collectĂ© via l'API officielle de Twitch dans postgresql, derriĂšre tout ça nous avons des scripts NodeJS et Python pour faire tout le travail. +Chaque visuel est planifiĂ© avant l'Ă©vĂ©nement sur Figma (c'est merveilleux). + +Voici un exemple de ce que nous avons conçu pour 2025 : + + + +Au fil des annĂ©es, nous avons augmentĂ© nos capacitĂ©s. Aujourd'hui, nous pouvons gĂ©rer jusqu'Ă  350 chaĂźnes en mĂȘme temps avec gĂ©nĂ©ration de mĂ©triques en temps rĂ©el. +L'Ă©vĂ©nement se termine le lundi Ă  1h du matin chaque annĂ©e, environ 5 heures plus tard, nous avons tous nos visuels prĂȘts. + +Depuis 2 ans maintenant, nous gĂ©nĂ©rons Ă©galement des infographies personnalisĂ©es pour chaque streamer, cette annĂ©e ce sont environ ~350 visuels qui ont Ă©tĂ© gĂ©nĂ©rĂ©s automatiquement. +Cette annĂ©e, tout est disponible [ici](https://stats.gdoc.fr/) + +### đŸ–Œïž Place Atlas + +Depuis 2022, l'organisation du ZEvent propose 2 jeux disponibles pour la durĂ©e de l'Ă©vĂ©nement : ZEventPlaysPokĂ©mon et ZEventPlace. + +Le premier est liĂ© aux statistiques des chaĂźnes mais le second est une pixel war pour la durĂ©e du week-end : 1€ = 10 pixels Ă  placer. +C'est vraiment amusant et cela crĂ©e une nouvelle façon pour les gens de donner pour une autre raison que la charitĂ© uniquement. + +Avec cela, nous avons dĂ©ployĂ© un outil qui collecte des images du canvas toutes les X minutes et affiche les descriptions que la communautĂ© soumet. +Ce workflow n'est pas parfait et nous avons changĂ© des choses en cours de route mais il fonctionne presque parfaitement depuis 2022. + +Vous pouvez voir l'atlas [ici](https://atlas.gdoc.fr/) + +## ❓ Qu'est-ce qui a changĂ© cette annĂ©e ? + +### đŸ€– AmĂ©lioration des capacitĂ©s et des technologies + +Parfois, NodeJS et Python ne suffisent pas. +Parce que je n'ai pas vraiment mis Ă  niveau mes scripts depuis 3 ans maintenant, une grande partie de ma stack concerne NodeJS et Python avec de mauvaises performances et un goĂ»t de scripts lancĂ©s sur des screens sans orchestration ni basculement. + +Cela a fonctionnĂ© pendant 3 ans, maintenant il fallait changer. +Avec cet objectif, j'ai commencĂ© Ă  conteneuriser une partie de ma stack pour ce ZEvent et j'ai commencĂ© Ă  refondre mes outils de collecte de statistiques avec rust et une conception master/slave. + +À l'Ă©poque, 1 script gĂ©rait chaque chaĂźne, maintenant nous avons un nƓud master qui stocke toutes les informations de chaĂźne et d'Ă©vĂ©nement. Les slaves se connectent au master pour collecter les donnĂ©es et les envoient via RabbitMQ pour ajouter une couche de buffer et de load-balancing. + +À la fin, les consommateurs rĂ©cupĂšrent les donnĂ©es de RabbitMQ et stockent les donnĂ©es dans MongoDB. + +**Pourquoi changer cela ?** + +Mon ancien systĂšme avait beaucoup de problĂšmes : +- Un seul script pour tout +- Aucun moyen d'Ă©quilibrer le flux de requĂȘtes +- Aucune rĂ©silience +- Une seule DB pour tout + +Maintenant, nous avons de petits services avec chacun son rĂŽle. +Si un service plante, il est remplacĂ© presque instantanĂ©ment par un autre, ce qui le rend rĂ©silient. + +Et si un problĂšme survient avec MongoDB ou les consommateurs, ce n'est pas grave : le RabbitMQ mettra les messages en buffer et en attente. +De grands changements concernant la DB ont amĂ©liorĂ© la façon dont les objets sont stockĂ©s mais aussi l'adaptabilitĂ© de la DB et les performances en utilisant de bons index. + +### 📩 Passer du bare metal Ă  K8S + +Les anciens services Ă©taient des scripts, maintenant j'ai des conteneurs avec registre et dĂ©ploiement helm spĂ©cifiquement pour cela. +Cela permet un dĂ©ploiement rapide et des changements faciles de nƓud. + +De plus, pas de soucis en cas de crashes maintenant : tout redĂ©marre tout seul. + +Pour le sĂ©curiser cette annĂ©e, 3 serveurs ont Ă©tĂ© dĂ©ployĂ©s pour gĂ©rer les applications et les sauvegardes afin de conserver toutes les donnĂ©es sans dĂ©faillance. + +### 👀 Est-ce que ça a fonctionnĂ© ? + +Oui, ça a parfaitement fonctionnĂ© ! Et mĂȘme si ce systĂšme n'Ă©tait pas le principal mais plutĂŽt une sauvegarde cette annĂ©e, il est en bonne voie pour devenir le systĂšme principal pour 2026. +Tungstene fera bientĂŽt partie des projets **Chronobreak** mais nous en parlerons un peu plus tard. + +## 📅 Prochains objectifs + +Ça s'est bien passĂ© cette annĂ©e mais beaucoup de choses pourraient ĂȘtre faites concernant mon travail. + +Pour l'annĂ©e prochaine, je prĂ©vois de mettre Ă  niveau les projets de collecte de statistiques avec de nouvelles façons d'afficher les donnĂ©es pour chaque spectateur. +J'aimerais uniformiser notre infrastructure et notre site web avec Maniarr pour connecter ensemble tous les sous-Ă©lĂ©ments de nos outils. + +Les prochains plans sont de dĂ©velopper une maniĂšre unique de gĂ©rer les autorisations, continuer Ă  mettre Ă  niveau mes scripts en conteneurs gĂ©nĂ©raux et ĂȘtre plus un Ops qu'un Dev. + +J'ai bon espoir que l'annĂ©e prochaine tout sera plus propre. +Des statistiques Ă  l'atlas, j'essaierai de vous tenir au courant et de travailler dessus rĂ©guliĂšrement. + +À bientĂŽt đŸ«Ą \ No newline at end of file diff --git a/src/pages/fr/blog/tags/[tag].astro b/src/pages/fr/blog/tags/[tag].astro new file mode 100644 index 0000000..c61b7d4 --- /dev/null +++ b/src/pages/fr/blog/tags/[tag].astro @@ -0,0 +1,41 @@ +--- +import Layout from '../../../../layouts/Layout.astro'; +import BlogPost from '../../../../components/blog/BlogPost.astro'; +import Heading from '../../../../components/ui/Heading.astro'; + +export async function getStaticPaths() { + const allPosts: any[] = await Astro.glob("../posts/*.md"); + + const uniqueTags: string[] = [ + ...new Set(allPosts.map((post: any) => post.frontmatter.tags).flat()), + ]; + + return uniqueTags.map((tag) => { + const filteredPosts = allPosts.filter((post) => + post.frontmatter.tags.includes(tag), + ); + return { + params: { tag }, + props: { posts: filteredPosts }, + }; + }); +} + +const { tag } = Astro.params; +const { posts } = Astro.props; +--- + + +
+
+ + { + posts.map((post) => ( + + )) + } +
+
+ +
+ diff --git a/src/pages/fr/blog/tags/index.astro b/src/pages/fr/blog/tags/index.astro new file mode 100644 index 0000000..29789f0 --- /dev/null +++ b/src/pages/fr/blog/tags/index.astro @@ -0,0 +1,19 @@ +--- +import Layout from "../../../../layouts/Layout.astro"; +import Tags from "../../../../components/blog/Tags.astro"; +import Heading from "../../../../components/ui/Heading.astro"; + +const pageTitle = "Tags"; + + + +--- + + +
+
+ + +
+
+
diff --git a/src/pages/fr/blog/techs/[category].astro b/src/pages/fr/blog/techs/[category].astro new file mode 100644 index 0000000..8e0d984 --- /dev/null +++ b/src/pages/fr/blog/techs/[category].astro @@ -0,0 +1,55 @@ +--- +import Layout from "../../../../layouts/Layout.astro"; +import BlogPost from "../../../../components/blog/BlogPost.astro"; +import Heading from "../../../../components/ui/Heading.astro"; +import type { MarkdownInstance } from 'astro'; + +interface Frontmatter { + languages: string[]; + title: string; + pubDate: string; + tags: string[]; + image: string; +} +export async function getStaticPaths(): Promise[]}}>>{ + const allPosts: MarkdownInstance[] = await Astro.glob("../posts/*.md"); + + const uniqueTags: string[] = [ + ...new Set(allPosts.map((post: MarkdownInstance) => post.frontmatter.languages).flat()), + ]; + + return uniqueTags.map((category) => { + const filteredPosts = allPosts.filter((post) => + post.frontmatter.languages.includes(category) + ); + return { + params: { category }, + props: { posts: filteredPosts }, + }; + }); +} + +const { category } = Astro.params; +const { posts } = Astro.props; +--- + + +
+
+ + + { + posts.map((post) => ( + + )) + } +
+
+
diff --git a/src/pages/fr/blog/techs/index.astro b/src/pages/fr/blog/techs/index.astro new file mode 100644 index 0000000..46adad9 --- /dev/null +++ b/src/pages/fr/blog/techs/index.astro @@ -0,0 +1,20 @@ +--- +import Layout from "../../../../layouts/Layout.astro"; +import Languages from "../../../../components/blog/Languages.astro"; +import Heading from "../../../../components/ui/Heading.astro"; + +const pageTitle = "Technologies"; // Fixed: Title was "Languages" but heading shows "Technologies" +const allPosts = await Astro.glob("../posts/*.md"); +const languages = [ + ...new Set(allPosts.map((post) => post.frontmatter.languages).flat()), +]; +--- + + +
+
+ + +
+
+
diff --git a/src/pages/fr/index.astro b/src/pages/fr/index.astro new file mode 100644 index 0000000..46c66dc --- /dev/null +++ b/src/pages/fr/index.astro @@ -0,0 +1,66 @@ +--- +import Layout from "../../layouts/Layout.astro"; +import Experience from "../../components/portfolio/Experience.astro"; +import HeroIndex from "../../components/portfolio/HeroIndex.astro"; +import Contact from "../../components/portfolio/Contact.astro"; +import ListProjects from "../../components/portfolio/ListProjects.astro"; +import { Icon } from "astro-icon/components"; +const pageTitle = "Portfolio - Pierre-Louis Leclerc | Proxyfil"; +const description = "Everything you need to know about my portfolio as a DevOps engineer student. Discover my experience, projects, and skills in development and technology."; +const ogimage = { + url: "/images/imagedefault.webp", + alt: "Capture d'écran d'un portfolio avec un design moderne et sombre. Featuring Pierre-Louis Leclerc, un étudiant en DevOps avec 3 ans d'expérience, mettant en avant sa passion pour le développement et la technologie. Comprend des sections de contact, des projets et une pile technologique avec des technologies telles que HTML5, JavaScript, TypeScript, Angular, Node.js, CSS, Tailwind, et plus encore.", +}; +import Heading from "../../components/ui/Heading.astro"; +--- + + + + +
+
+
+ + +
+ + +
+
+ +
+
+ +
+ + +
+

+ J'aime transformer des idées en projets réels. +
Ici, je vous présente certains des projets sur lesquels j'ai travaillé, en appliquant différentes technologies, un peu de design et beaucoup de créativité. + Découvrez-les ! +

+
+
+
+ + + +
diff --git a/src/pages/fr/portfolio/projects/ingdoc.md b/src/pages/fr/portfolio/projects/ingdoc.md new file mode 100644 index 0000000..8dff061 --- /dev/null +++ b/src/pages/fr/portfolio/projects/ingdoc.md @@ -0,0 +1,43 @@ +--- +layout: /src/layouts/ProjectLayout.astro +title: 'InGDoc' +pubDate: 2025-04-05 +description: "CrĂ©ation d'outils pour aider Ă  la visibilitĂ© des Ă©vĂ©nements et rĂ©aliser des visuels percutants." +languages: ["vue", "node", "python","figma"] +image: + url: "/images/projects/ingdoc.webp" + alt: "Miniature d'InGDoc." +--- + +**InGDoc** est un groupe communautaire créé dans l'idĂ©e de fournir Ă  certains grands Ă©vĂ©nements Twitch des outils pour les rendre facilement regardables et fournir des statistiques Ă  leur sujet. + +## 📖 Historique + +Créée en 2020, l'Ă©quipe a Ă©tĂ© fondĂ©e Ă  l'origine pour suivre le ZEvent2020. Notre premier outil Ă©tait un Google Document qui suivait la progression des objectifs de dons et les montants collectĂ©s. +En **2022**, nous commençons Ă  collecter plus de donnĂ©es sur les streams et crĂ©ons des infographies Ă  la fin des diffĂ©rents Ă©vĂ©nements que nous suivons. + +Nous avons travaillĂ© avec certaines organisations pour crĂ©er des outils personnalisĂ©s et nous continuons Ă  donner vie aux idĂ©es de la communautĂ©. +Notre site web est une collection de tout le travail que nous avons accompli jusqu'Ă  prĂ©sent, comme la crĂ©ation d'atlas pour les **Ă©vĂ©nements type r/place** ou des Google Sheets entiers pour suivre les **Progressions WoW**. +Vous pouvez Ă©galement voir l'effort de documentation fait par la communautĂ© pour suivre **RPZ**, un Ă©vĂ©nement de roleplay sur GTA V qui a rĂ©uni plus de 100 streamers venus jouer et crĂ©er des histoires pendant 2 semaines d'affilĂ©e. + +## 📱 ÉvĂ©nements suivis + +- ZEvent2020 +- Progress WoW Shadowlands +- RPZ +- ZRT Trackmania Cup 2021 +- ZEvent2021 +- Progress WoW Dragonflight +- ZEvent2022 +- ZRT Trackmania Cup 2022 +- Speedons 2024 +- WeekAndArt 2024 +- ZEvent2024 +- Progress WoW The War Within +- Stream For Humanity +- WeekAndArt 2025 + +## 🌐 Liens utiles + +- [Site web](https://gdoc.fr/) +- [Twitter](https://x.com/Les_InGdoc) diff --git a/src/pages/fr/portfolio/projects/pollpy.md b/src/pages/fr/portfolio/projects/pollpy.md new file mode 100644 index 0000000..4523870 --- /dev/null +++ b/src/pages/fr/portfolio/projects/pollpy.md @@ -0,0 +1,31 @@ +--- +layout: /src/layouts/ProjectLayout.astro +title: 'PollPy' +pubDate: 2024-08-21 +description: 'Bot Discord pour crĂ©er des prĂ©dictions et construire une communautĂ© autour.' +languages: ["nodejs", "git", "bash"] +image: + url: "/images/projects/pollpy.webp" + alt: "Miniature du projet PollPy." +--- + +**PollPy** Ă©tait un bot Discord pour implĂ©menter des prĂ©dictions et des sondages de maniĂšre simple pour les utilisateurs finaux. Il Ă©tait conçu pour ĂȘtre amusant avec des points virtuels et diffĂ©rents types de prĂ©dictions. + +Construit en un Ă©tĂ©, il Ă©tait vraiment basique mais a reçu quelques mises Ă  jour au fil des annĂ©es pour ajouter des fonctionnalitĂ©s et est passĂ© de quelques serveurs Ă  plus de 100 l'utilisant avec plus de 10 000 utilisateurs. + +## đŸ§© FonctionnalitĂ©s + +- CrĂ©er des prĂ©dictions et permettre aux utilisateurs de participer +- Les clĂŽturer et redistribuer les points + +## 💡 Technologies utilisĂ©es + +- NodeJS +- Supabase + +## 🎯 Objectif + +Ce bot rĂ©pondait Ă  un besoin de bot de prĂ©diction Ă  une pĂ©riode oĂč Discord n'avait pas encore créé de fonctionnalitĂ© pour cela. + + +🚀 *DĂ©veloppĂ© par Proxyfil.* diff --git a/src/pages/fr/portfolio/projects/tungstene_enriched.md b/src/pages/fr/portfolio/projects/tungstene_enriched.md new file mode 100644 index 0000000..4ceea63 --- /dev/null +++ b/src/pages/fr/portfolio/projects/tungstene_enriched.md @@ -0,0 +1,42 @@ +--- +layout: /src/layouts/ProjectLayout.astro +title: 'Tungstene Enriched' +pubDate: 2025-02-09 +description: 'Tungstene Enriched est un outil pour collecter, transformer et restituer des donnĂ©es de Twitch.' +languages: ["nodejs", "cloudflare", "postgresql", "python", "vue"] +image: + url: "/images/projects/tungstene.webp" + alt: "Miniature de Tungstene." +--- + +**Tungstene Enriched** est un outil pour collecter, transformer et restituer des donnĂ©es de Twitch. L'outil est principalement axĂ© sur le chat IRC mais collecte Ă©galement des donnĂ©es depuis l'API Twitch. +Il utilise **NodeJS** pour se connecter Ă  toutes les sources de donnĂ©es et stocke les informations dans une base de donnĂ©es **Postgresql**. + +Aujourd'hui ĂągĂ© de 4 ans avec 1 refonte, le projet doit ĂȘtre remis aux nouveaux standards dans les prochains mois. + +## đŸ§© FonctionnalitĂ©s + +- Collecte de messages depuis l'IRC Twitch +- Collecte de donnĂ©es depuis l'API Twitch +- Stockage des donnĂ©es dans Postgresql +- Transformation des donnĂ©es pour crĂ©er des rĂ©sumĂ©s +- RĂ©cupĂ©ration des donnĂ©es et crĂ©ation d'infographies avec celles-ci + +## 💡 Technologies utilisĂ©es + +- NodeJS +- Postgresql +- VueJS +- Python + +## 🌐 DĂ©mo + +👉 [Voir Ă  quoi cela sert](https://x.com/Les_InGdoc/status/1914590807941689346) + +## 🎯 Objectif + +Le but principal de ce projet Ă©tait d'apprendre Ă  interagir avec des API et de dĂ©couvrir Ă  quel point certains Ă©vĂ©nements sont importants sur Twitch. +Ces donnĂ©es sont utilisĂ©es pour crĂ©er des infographies et aider la communautĂ© Ă  interagir ensemble. + + +🚀 *DĂ©veloppĂ© par Proxyfil.* diff --git a/src/pages/fr/robots.txt.ts b/src/pages/fr/robots.txt.ts new file mode 100644 index 0000000..d985f35 --- /dev/null +++ b/src/pages/fr/robots.txt.ts @@ -0,0 +1,15 @@ +import type { APIRoute } from 'astro'; + +const getRobotsTxt = (sitemapURL: URL) => ` +User-agent: * +Allow: / + +Sitemap: ${sitemapURL.href} + + +`; + +export const GET: APIRoute = ({ site }) => { + const sitemapURL = new URL('sitemap-index.xml', site); + return new Response(getRobotsTxt(sitemapURL)); +}; \ No newline at end of file diff --git a/src/pages/fr/rss.xml.js b/src/pages/fr/rss.xml.js new file mode 100644 index 0000000..1cc4230 --- /dev/null +++ b/src/pages/fr/rss.xml.js @@ -0,0 +1,11 @@ +import rss, { pagesGlobToRssItems } from '@astrojs/rss'; + +export async function GET(context) { + return rss({ + title: 'DevOps and Technology Blog | Pierre-Louis Leclerc | Proxyfil', + description: 'Welcome to my blog, where I share my passion for computer science, DevOps, and the latest technology trends.', + site: context.site, + items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')), + customData: `en`, + }); +} \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index 4ad87f6..2df6db6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,66 +1,5 @@ --- -import Layout from "../layouts/Layout.astro"; -import Experience from "../components/portfolio/Experience.astro"; -import HeroIndex from "../components/portfolio/HeroIndex.astro"; -import Contact from "../components/portfolio/Contact.astro"; -import ListProjects from "../components/portfolio/ListProjects.astro"; -import { Icon } from "astro-icon/components"; -const pageTitle = "Portfolio - Pierre-Louis Leclerc | Proxyfil"; -const description = "Everything you need to know about my portfolio as a DevOps engineer student. Discover my experience, projects, and skills in development and technology."; -const ogimage = { - url: "/images/imagedefault.webp", - alt: "Screenshot of a web portfolio with a modern and dark design. Featuring Pierre-Louis Leclerc, a DevOps engineer student with 3 years of experience, highlighting his passion for development and technology. Includes contact sections, projects, and a technology stack with technologies like HTML5, JavaScript, TypeScript, Angular, Node.js, CSS, Tailwind, and more.", -}; -import Heading from "../components/ui/Heading.astro"; ---- - - - - -
-
-
- - -
- - -
-
- -
-
- -
- - -
-

- I love turning ideas into real projects. -
Here I show you some of the developments I've worked on, applying technology, design, and lots of creativity. - Check them out! -

-
-
-
+--- - -
+ \ No newline at end of file