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/public/images/posts/minecraft-kubernetes.webp b/public/images/posts/minecraft-kubernetes.webp new file mode 100644 index 0000000..52e9170 Binary files /dev/null and b/public/images/posts/minecraft-kubernetes.webp differ 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/en/blog/posts/minecraft-kubernetes.md b/src/pages/en/blog/posts/minecraft-kubernetes.md new file mode 100644 index 0000000..f766ab8 --- /dev/null +++ b/src/pages/en/blog/posts/minecraft-kubernetes.md @@ -0,0 +1,93 @@ +--- +layout: /src/layouts/MarkdownPostLayout.astro +title: Creating a Minecraft Server with Kubernetes +author: Pierre-Louis Leclerc | Proxyfil +description: "Minecraft is a popular game that can be hosted in different ways. Let's discover how to deploy a Minecraft server using Kubernetes! đŸŽźâ˜ïž" +image: + url: "/images/posts/minecraft-kubernetes.webp" + alt: "Article illustration" +pubDate: 2025-11-05 +tags: + [ + "Retex", "System", "Kubernetes" + ] +languages: ["kubernetes", "bash", "docker"] +--- + +For several years now, Minecraft has been my favorite game. I can't count the hours I've spent on it, whether solo or multiplayer with friends. + +A long time ago, I hosted Minecraft servers on classic VPS, but with the evolution of technology, I decided to explore Kubernetes to deploy my Minecraft server. +So I went from a classic VPS infrastructure with daemons to migrate to Kubernetes, taking advantage of this change to perform a major Minecraft version upgrade. + +## 🚛 Moving from Bare Metal to Kubernetes + +### ❓ Why Kubernetes? + +Kubernetes is a container orchestration platform that offers many advantages for hosting applications, including game servers like Minecraft. +We can note some key advantages such as scalability, resilience and ease of resource management. + +The main reason was curiosity to explore Kubernetes and see how it could be used to host a Minecraft server. Resources already existed to deploy Minecraft servers on Docker, so the next logical step was to move to Kubernetes. + +Moreover, I already have a trajectory to move towards a cloud provider-independent infrastructure, already having a Kubernetes node at home, it was the perfect opportunity to perform this migration. + + +### 📜 Infrastructure Definition + +To host these Minecraft servers (or rather an entire network in this case) I used a machine with an Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz, 32GB of RAM and 256GB of SSD storage. + +To deploy a Minecraft server network, the historical solution was BungeeCord. However, for a few years now, Velocity (with its predecessor Waterfall) has been the favored solution. +Velocity acts as a proxy between players and Minecraft servers, allowing efficient connection management and load balancing between multiple servers or seamless movement from instance to instance within the same network. + +Behind Velocity, I deployed several Minecraft servers with different roles: +- A Lobby server: The main entry point for players, where they can prepare before joining other servers. +- A Survival server: A server dedicated to survival mode + +For now nothing more but the configuration remains flexible to add other servers in the future. + +### đŸ–„ïž Deployment + +For deployment, all resources used are available on [my GitHub repo](https://github.com/Proxyfil/Deployments/tree/main/minecraft). + +Each server is deployed as a Kubernetes StatefulSet and exposes a particular port for communication with Minecraft clients. + +All storage is done directly on the host node's filesystem via PersistentVolumes linked to PersistentVolumeClaims. It's true that this approach is not ideal for horizontal scaling but for personal use it's sufficient. +A future improvement could be to use network storage like NFS or Ceph to allow better flexibility. + +Each server uses a custom Docker image based on an existing Minecraft image, with specific configurations for each server type. (Thanks to [itzg](https://github.com/itzg/docker-minecraft-server) who produces considerable work with other contributors!) + +### 🔐 Connection Issue + +Minecraft by default uses port 25565 for incoming connections. However Kubernetes does not allow directly exposing port 25565 via a LoadBalancer or an Ingress. + +To work around this problem I deployed an Nginx container as a LoadBalancer that redirects incoming traffic on port 25565 to the port exposed by the Kubernetes service. + +## âŹ†ïž Improvements + +### 🚀 Migration to Version 1.21.10 + +In addition to the migration to Kubernetes, I took the opportunity to perform a major Minecraft version upgrade, going from version 1.20.4 to version 1.21.10. + +This latest version being very recent (at the time of writing), I had to make sure that all the plugins used were compatible with this version. + +Fortunately, most popular plugins had already been updated to be compatible with version 1.21.x, but I had to make some minor adjustments in the configuration of some plugins to ensure full compatibility. + +Despite this, the deployment remains quite unstable from one minor version to another, I imagine this will improve when updates move to full version and no longer beta or alpha. + +### 📊 Monitoring + +To monitor the performance and health of my Minecraft server, I integrated monitoring tools into my Kubernetes cluster. I use Prometheus to collect metrics and Grafana to visualize this data. + +This allows me to track resource usage, connection latency and other key performance indicators, which is crucial to ensure a smooth gaming experience for players. +All deployment resources are available on [my GitHub repo](https://github.com/Proxyfil/Deployments/tree/main/monitoring/prometheus) with the necessary configurations to deploy Prometheus in Kubernetes. + +I also added a Grafana dashboard specific to Minecraft, which displays information such as the number of connected players, memory and CPU usage, and other relevant metrics. +With this I also have a Node Exporter to monitor the state of the host node. + +## 📅 Next Objectives + +Now that things are stable, I have some ideas to further improve my Minecraft deployment on Kubernetes. +We could move to provisioned storage, improve monitoring with alerts or add metrics with plugins. + +But for now I'm quite satisfied with the current configuration and I'm fully enjoying my Minecraft server hosted on Kubernetes! + +See you soon đŸ«Ą 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/minecraft-kubernetes.md b/src/pages/fr/blog/posts/minecraft-kubernetes.md new file mode 100644 index 0000000..6180fbf --- /dev/null +++ b/src/pages/fr/blog/posts/minecraft-kubernetes.md @@ -0,0 +1,93 @@ +--- +layout: /src/layouts/MarkdownPostLayout.astro +title: CrĂ©er un serveur Minecraft avec Kubernetes +author: Pierre-Louis Leclerc | Proxyfil +description: "Minecraft est un jeu populaire qui peut ĂȘtre hĂ©bergĂ© de diffĂ©rentes maniĂšres. DĂ©couvrons comment dĂ©ployer un serveur Minecraft Ă  l'aide de Kubernetes ! đŸŽźâ˜ïž" +image: + url: "/images/posts/minecraft-kubernetes.webp" + alt: "Illustration de l'article" +pubDate: 2025-11-05 +tags: + [ + "Retex", "System", "Kubernetes" + ] +languages: ["kubernetes", "bash", "docker"] +--- + +Depuis quelques annĂ©es maintenant Minecraft est mon jeu de coeur. Je ne compte plus les heures passĂ©es dessus, que ce soit en solo ou en multijoueur avec des amis. + +Il y a bien longtemps, j'ai hĂ©bergĂ© des serveurs Minecraft sur des VPS classiques, mais avec l'Ă©volution de la technologie, j'ai dĂ©cidĂ© d'explorer Kubernetes pour dĂ©ployer mon serveur Minecraft. +Je suis donc parti d'une infrastructure VPS classique avec des daemons pour migrer vers Kubernetes, en profitant au passage de ce changement pour effectuer une montĂ©e de version majeure de Minecraft. + +## 🚛 Passage de bare metal Ă  Kubernetes + +### ❓ Pourquoi Kubernetes ? + +Kubernetes est une plateforme d'orchestration de conteneurs qui offre de nombreux avantages pour hĂ©berger des applications, y compris des serveurs de jeux comme Minecraft. +On notera certains avantages clĂ©s comme le scalabilitĂ©, la rĂ©silience et la facilitĂ© de gestion des ressources. + +La principale raison Ă©tait surtout la curiositĂ© d'explorer Kubernetes et de voir comment il pouvait ĂȘtre utilisĂ© pour hĂ©berger un serveur Minecraft. Des ressources existaient dĂ©jĂ  pour dĂ©ployer des serveurs Minecraft sur Docker, donc l'Ă©tape suivante logique Ă©tait de passer Ă  Kubernetes. + +De plus, j'ai dĂ©jĂ  pour trajectoire de passer vers une infrastructure indĂ©pendante des cloud providers, ayant dĂ©jĂ  un noeud Kubernetes chez moi, c'Ă©tait l'occasion parfaite pour effectuer cette migration. + + +### 📜 DĂ©finition de l'infrastructure + +Pour hĂ©berger ces serveurs minecraft (ou plutĂŽt un network entier dans ce cas) j'ai utilisĂ© une machine avec un Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz, 32Go de RAM et 256Go de stockage SSD. + +Pour dĂ©ployer un network de serveurs Minecraft la solution historique Ă©tait BungeeCord. Cependant depuis quelques annĂ©es maintenant c'est Velocity (avec son prĂ©dĂ©cesseur Waterfall) qui est la solution favorisĂ©e. +Velocity agit comme un proxy entre les joueurs et les serveurs Minecraft, permettant une gestion efficace des connexions et une rĂ©partition de la charge entre plusieurs serveurs ou du dĂ©placement sans dĂ©connexion d'instance Ă  instance au sein du mĂȘme network. + +DerriĂšre Velocity, j'ai dĂ©ployĂ© plusieurs serveurs Minecraft avec des rĂŽles diffĂ©rents : +- Un serveur Lobby : Le point d'entrĂ©e principal pour les joueurs, oĂč ils peuvent se prĂ©parer avant de rejoindre d'autres serveurs. +- Un serveur Survival : Un serveur dĂ©diĂ© au mode survie + +Pour l'instant rien de plus mais la configuration reste flexible pour ajouter d'autres serveurs Ă  l'avenir. + +### đŸ–„ïž DĂ©ploiement + +Pour le dĂ©ploiement toutes les ressources utilisĂ©es sont disponibles sur [mon repo GitHub](https://github.com/Proxyfil/Deployments/tree/main/minecraft). + +Chaque serveur est dĂ©ployĂ© en tant que StatefulSet Kubernetes et expose un port particulier pour la communication avec les clients Minecraft. + +Tout le stockage se fait directement sur le filesystem du noeud hĂŽte via des PersistentVolumes liĂ©s Ă  des PersistentVolumeClaims. Il est vrai que cette approche n'est pas idĂ©ale en cas de scaling horizontal mais pour un usage personnel c'est suffisant. +Une amĂ©lioration future pourrait ĂȘtre d'utiliser un stockage en rĂ©seau comme NFS ou Ceph pour permettre une meilleure flexibilitĂ©. + +Chaque serveur utilise une image Docker personnalisĂ©e basĂ©e sur une image Minecraft existante, avec des configurations spĂ©cifiques pour chaque type de serveur. (Merci Ă  [itzg](https://github.com/itzg/docker-minecraft-server) qui produit un travail considĂ©rable avec les autres contributeurs !) + +### 🔐 ProblĂ©matique de connection + +Minecraft par dĂ©faut utilise le port 25565 pour les connexions entrantes. Cependant Kubernetes ne permet pas d'exposer directement le port 25565 via un LoadBalancer ou un Ingress. + +Pour contourner ce problĂšme j'ai dĂ©ployĂ© un conteneur Nginx en tant que LoadBalancer qui redirige le trafic entrant sur le port 25565 vers le port qui est exposĂ© par le service Kubernetes. + +## âŹ†ïž AmĂ©liorations + +### 🚀 Migration vers la version 1.21.10 + +En plus de la migration vers Kubernetes, j'ai profitĂ© de l'occasion pour effectuer une montĂ©e de version majeure de Minecraft, passant de la version 1.20.4 Ă  la version 1.21.10. + +Cette derniĂšre version Ă©tant trĂšs rĂ©cente (Ă  l'heure oĂč j'Ă©cris ces lignes), j'ai dĂ» m'assurer que tous les plugins utilisĂ©s Ă©taient compatibles avec cette version. + +Heureusement, la plupart des plugins populaires avaient dĂ©jĂ  Ă©tĂ© mis Ă  jour pour ĂȘtre compatibles avec la version 1.21.x, mais j'ai dĂ» faire quelques ajustements mineurs dans la configuration de certains plugins pour garantir une compatibilitĂ© totale. + +MalgrĂ© ceci le dĂ©ploiement reste assez instable d'une version mineure Ă  une autre, j'imagine que cela s'amĂ©liorera lorsque les mises Ă  jour passeront en version complĂšte et non plus en bĂȘta ou alpha. + +### 📊 Monitoring + +Pour surveiller les performances et la santĂ© de mon serveur Minecraft, j'ai intĂ©grĂ© des outils de monitoring dans mon cluster Kubernetes. J'utilise Prometheus pour collecter les mĂ©triques et Grafana pour visualiser ces donnĂ©es. + +Cela me permet de suivre l'utilisation des ressources, la latence des connexions et d'autres indicateurs clĂ©s de performance, ce qui est crucial pour assurer une expĂ©rience de jeu fluide pour les joueurs. +Toutes les ressources de dĂ©ploiement sont disponibles sur [mon repo GitHub](https://github.com/Proxyfil/Deployments/tree/main/monitoring/prometheus) avec les configurations nĂ©cessaires pour dĂ©ployer Prometheus dans Kubernetes. + +J'ai aussi ajoutĂ© un dashboard Grafana spĂ©cifique pour Minecraft, qui affiche des informations telles que le nombre de joueurs connectĂ©s, l'utilisation de la mĂ©moire et du CPU, et d'autres mĂ©triques pertinentes. +Avec cela j'ai aussi un Node Exporter pour surveiller l'Ă©tat du noeud hĂŽte. + +## 📅 Prochains objectifs + +Maintenant que les choses sont stables, j'ai quelques idĂ©es pour amĂ©liorer encore mon dĂ©ploiement Minecraft sur Kubernetes. +On pourrait passer sur du stockage provisionnĂ©, amĂ©liorer le monitoring avec des alertes ou ajouter des metrics avec des plugins. + +Mais pour l'instant je suis assez satisfait de la configuration actuelle et je profite pleinement de mon serveur Minecraft hĂ©bergĂ© sur Kubernetes ! + +À bientĂŽt đŸ«Ą \ No newline at end of file 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