From dc3fb4f3d8dd968ef7e5be6297f020049a1338ea Mon Sep 17 00:00:00 2001 From: Jalil Arfaoui Date: Wed, 7 Jan 2026 01:45:40 +0100 Subject: [PATCH] Ajout de la section galerie photo et nettoyage du template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Galerie photo : - Ajout du layout photo avec slideshow plein écran - Navigation par catégories (portraits, paysages, nature, etc.) - Section "Fil Photo" avec posts illustrés (photoBlogPosts) - Lightbox pour les albums de catégories - Composants : Slideshow, CategoryNav, CategoryGrid, Lightbox, MasonryGallery Nettoyage : - Suppression du contenu démo du template (posts, images, about) - Consolidation src/collections/ dans src/data/ - Suppression du config.js dupliqué (garde config.ts) - Nettoyage des assets inutilisés (posts/, experiences/) Corrections : - Favicon récupéré du site actuel - Chemins favicon corrigés dans les layouts UI : - Page d'accueil mise à jour - Header/Footer simplifiés - Nouvelle page À propos --- .env.example | 4 + .gitignore | 3 + astro.config.mjs | 7 + package-lock.json | 850 +++++++++++++++++- package.json | 10 +- public/assets/images/favicon.png | Bin 3312 -> 2304 bytes public/favicon.ico | Bin 15406 -> 2304 bytes scripts/fetch-images.ts | 87 ++ src/collections/.gitkeep | 0 src/collections/experiences.json | 23 - src/collections/menu.json | 18 - src/components/DarkModeToggle.astro | 55 ++ src/components/LanguageSwitcher.astro | 75 ++ src/components/Link.astro | 16 + src/components/footer.astro | 123 +-- src/components/header-i18n.astro | 211 +++++ src/components/header.astro | 80 +- src/components/home/projects.astro | 2 +- src/components/icons/HomeIcon.astro | 23 + src/components/logo.astro | 25 +- src/components/photo/AlbumHeader.astro | 155 ++++ src/components/photo/CategoryGrid.astro | 307 +++++++ src/components/photo/CategoryNav.astro | 251 ++++++ src/components/photo/Lightbox.astro | 329 +++++++ src/components/photo/MasonryGallery.astro | 78 ++ src/components/photo/PhotoGallery.astro | 255 ++++++ src/components/photo/ScrollIndicator.astro | 79 ++ src/components/photo/SlideControls.astro | 92 ++ src/components/photo/Slideshow.astro | 141 +++ src/content/config.js | 14 - src/content/config.ts | 85 ++ src/content/photoBlogPosts/enigma.md | 15 + src/content/photoBlogPosts/eroll.md | 15 + src/content/photoBlogPosts/field-of-stones.md | 15 + src/content/photoBlogPosts/helsinki.md | 15 + src/content/photoBlogPosts/ifrane-hike.md | 15 + src/content/photoBlogPosts/inox-park-2011.md | 15 + src/content/photoBlogPosts/london-calling.md | 15 + .../photoBlogPosts/no-wind-las-cuevas.md | 15 + .../schoolbag-operation-2012.md | 15 + .../photoBlogPosts/sequanian-sunday.md | 15 + src/content/photoBlogPosts/tangier-walk.md | 15 + .../photoBlogPosts/waiting-for-the-bride.md | 15 + .../wandering-tangier-medina.md | 15 + .../photoBlogPosts/wedding-aurore-thomas.md | 11 + src/content/photoCategories/cultures.json | 5 + src/content/photoCategories/engines.json | 5 + src/content/photoCategories/everyday.json | 5 + src/content/photoCategories/music.json | 5 + src/content/photoCategories/nature.json | 5 + src/content/photoCategories/places.json | 5 + src/content/photoCategories/portraits.json | 5 + src/content/photoCategories/sports.json | 5 + .../post/ai-remove-image-background.md | 18 - src/content/post/astro-aria.md | 14 - src/content/post/broadcast-channel.md | 32 - .../cloudflare-web-analytics-kill-adblock.md | 70 -- src/content/post/cloudflare-worker-image.md | 104 --- src/content/post/deploy-fediverse-instance.md | 48 - src/content/post/dns-surf.md | 21 - src/content/post/email-ml.md | 26 - src/content/post/github-og-image.md | 54 -- .../post/google-safe-browsing-alternative.md | 64 -- ...ooooooooooooooooooooooooooooooooooooong.md | 20 - src/content/post/sink.md | 60 -- src/content/post/umami-kill-adblock.md | 52 -- src/content/post/vercel-edge-image.md | 80 -- src/content/post/vercel-kill-adblock.md | 44 - src/data/experiences.json | 37 + src/data/favorites.json | 8 + src/data/menu.json | 10 + src/data/menu.ts | 138 +++ src/{collections => data}/projects.json | 6 - src/layouts/PhotoLayout.astro | 44 + src/layouts/main.astro | 2 +- src/pages/a-propos.astro | 54 ++ src/pages/about.astro | 72 -- src/pages/ar/index.astro | 181 ++++ src/pages/ar/نبذة-عني.astro | 54 ++ src/pages/en/about.astro | 54 ++ src/pages/en/index.astro | 181 ++++ src/pages/index.astro | 213 +++-- src/pages/photo.astro | 10 + src/pages/photo/albums/[...category].astro | 36 + src/pages/photo/blog/[slug].astro | 115 +++ src/pages/photo/blog/index.astro | 318 +++++++ src/pages/projects.astro | 2 +- src/utils/i18n.ts | 107 +++ 88 files changed, 4969 insertions(+), 1024 deletions(-) create mode 100644 .env.example create mode 100644 scripts/fetch-images.ts delete mode 100644 src/collections/.gitkeep delete mode 100644 src/collections/experiences.json delete mode 100644 src/collections/menu.json create mode 100644 src/components/DarkModeToggle.astro create mode 100644 src/components/LanguageSwitcher.astro create mode 100644 src/components/Link.astro create mode 100644 src/components/header-i18n.astro create mode 100644 src/components/icons/HomeIcon.astro create mode 100644 src/components/photo/AlbumHeader.astro create mode 100644 src/components/photo/CategoryGrid.astro create mode 100644 src/components/photo/CategoryNav.astro create mode 100644 src/components/photo/Lightbox.astro create mode 100644 src/components/photo/MasonryGallery.astro create mode 100644 src/components/photo/PhotoGallery.astro create mode 100644 src/components/photo/ScrollIndicator.astro create mode 100644 src/components/photo/SlideControls.astro create mode 100644 src/components/photo/Slideshow.astro delete mode 100644 src/content/config.js create mode 100644 src/content/config.ts create mode 100644 src/content/photoBlogPosts/enigma.md create mode 100644 src/content/photoBlogPosts/eroll.md create mode 100644 src/content/photoBlogPosts/field-of-stones.md create mode 100644 src/content/photoBlogPosts/helsinki.md create mode 100644 src/content/photoBlogPosts/ifrane-hike.md create mode 100644 src/content/photoBlogPosts/inox-park-2011.md create mode 100644 src/content/photoBlogPosts/london-calling.md create mode 100644 src/content/photoBlogPosts/no-wind-las-cuevas.md create mode 100644 src/content/photoBlogPosts/schoolbag-operation-2012.md create mode 100644 src/content/photoBlogPosts/sequanian-sunday.md create mode 100644 src/content/photoBlogPosts/tangier-walk.md create mode 100644 src/content/photoBlogPosts/waiting-for-the-bride.md create mode 100644 src/content/photoBlogPosts/wandering-tangier-medina.md create mode 100644 src/content/photoBlogPosts/wedding-aurore-thomas.md create mode 100644 src/content/photoCategories/cultures.json create mode 100644 src/content/photoCategories/engines.json create mode 100644 src/content/photoCategories/everyday.json create mode 100644 src/content/photoCategories/music.json create mode 100644 src/content/photoCategories/nature.json create mode 100644 src/content/photoCategories/places.json create mode 100644 src/content/photoCategories/portraits.json create mode 100644 src/content/photoCategories/sports.json delete mode 100644 src/content/post/ai-remove-image-background.md delete mode 100644 src/content/post/astro-aria.md delete mode 100644 src/content/post/broadcast-channel.md delete mode 100644 src/content/post/cloudflare-web-analytics-kill-adblock.md delete mode 100644 src/content/post/cloudflare-worker-image.md delete mode 100644 src/content/post/deploy-fediverse-instance.md delete mode 100644 src/content/post/dns-surf.md delete mode 100644 src/content/post/email-ml.md delete mode 100644 src/content/post/github-og-image.md delete mode 100644 src/content/post/google-safe-browsing-alternative.md delete mode 100644 src/content/post/looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong.md delete mode 100644 src/content/post/sink.md delete mode 100644 src/content/post/umami-kill-adblock.md delete mode 100644 src/content/post/vercel-edge-image.md delete mode 100644 src/content/post/vercel-kill-adblock.md create mode 100644 src/data/experiences.json create mode 100644 src/data/favorites.json create mode 100644 src/data/menu.json create mode 100644 src/data/menu.ts rename src/{collections => data}/projects.json (70%) create mode 100644 src/layouts/PhotoLayout.astro create mode 100644 src/pages/a-propos.astro delete mode 100644 src/pages/about.astro create mode 100644 src/pages/ar/index.astro create mode 100644 src/pages/ar/نبذة-عني.astro create mode 100644 src/pages/en/about.astro create mode 100644 src/pages/en/index.astro create mode 100644 src/pages/photo.astro create mode 100644 src/pages/photo/albums/[...category].astro create mode 100644 src/pages/photo/blog/[slug].astro create mode 100644 src/pages/photo/blog/index.astro create mode 100644 src/utils/i18n.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6c10bdb --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +WEBDAV_URL=https://nas.arfaoui.net:6006 +WEBDAV_PATH=/photo/Portfolio +WEBDAV_USER=your_username +WEBDAV_PASS=your_password \ No newline at end of file diff --git a/.gitignore b/.gitignore index 16d54bb..15b687c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ pnpm-debug.log* .env .env.production +# images fetched from NAS +src/assets/images/photos/ + # macOS-specific files .DS_Store diff --git a/astro.config.mjs b/astro.config.mjs index 1886bb6..2148d5c 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -5,4 +5,11 @@ import tailwind from "@astrojs/tailwind"; // https://astro.build/config export default defineConfig({ integrations: [tailwind()], + i18n: { + defaultLocale: "fr", + locales: ["fr", "en", "ar"], + routing: { + prefixDefaultLocale: false + } + } }); diff --git a/package-lock.json b/package-lock.json index 5c68e60..8dd26e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,13 @@ "@astrojs/tailwind": "^5.1.0", "@biomejs/biome": "1.7.3", "@tailwindcss/typography": "^0.5.13", + "@types/node": "^25.0.3", "astro": "^4.8.2", + "dotenv": "^17.2.3", "tailwindcss": "^3.4.3", - "typescript": "^5.4.5" + "tsx": "^4.21.0", + "typescript": "^5.4.5", + "webdav": "^5.8.0" } }, "node_modules/@alloc/quick-lru": { @@ -670,6 +674,16 @@ "node": ">=14.21.3" } }, + "node_modules/@buttercup/fetch": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", + "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "node-fetch": "^3.3.0" + } + }, "node_modules/@emmetio/abbreviation": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", @@ -1007,6 +1021,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -1023,6 +1054,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -1039,6 +1087,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -1958,6 +2023,16 @@ "@types/unist": "*" } }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2494,6 +2569,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz", + "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==", + "dev": true, + "license": "MIT" + }, "node_modules/camelcase": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", @@ -2589,6 +2671,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2895,6 +2987,16 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2907,6 +3009,16 @@ "node": ">=4" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -3008,6 +3120,19 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dset": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", @@ -3222,6 +3347,25 @@ "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3231,6 +3375,30 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3303,6 +3471,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3381,6 +3562,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -3670,6 +3864,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hot-patcher": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.1.tgz", + "integrity": "sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -3730,6 +3931,13 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", @@ -3991,6 +4199,13 @@ "node": ">=6" } }, + "node_modules/layerr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz", + "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==", + "dev": true, + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", @@ -4169,6 +4384,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/mdast-util-definitions": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", @@ -5091,6 +5318,13 @@ "node": ">= 10" } }, + "node_modules/nested-property": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", + "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==", + "dev": true, + "license": "MIT" + }, "node_modules/nlcst-to-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", @@ -5104,6 +5338,46 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -5368,6 +5642,13 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", + "dev": true, + "license": "ISC" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -5672,6 +5953,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5879,6 +6167,13 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5896,6 +6191,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -6372,6 +6677,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -6584,6 +6902,459 @@ "dev": true, "optional": true }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -6624,6 +7395,13 @@ "semver": "^7.3.8" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -6796,6 +7574,27 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7165,6 +7964,55 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webdav": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.8.0.tgz", + "integrity": "sha512-iuFG7NamJ41Oshg4930iQgfIpRrUiatPWIekeznYgEf2EOraTRcDPTjy7gIOMtkdpKTaqPk1E68NO5PAGtJahA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@buttercup/fetch": "^0.2.1", + "base-64": "^1.0.0", + "byte-length": "^1.0.2", + "entities": "^6.0.0", + "fast-xml-parser": "^4.5.1", + "hot-patcher": "^2.0.1", + "layerr": "^3.0.0", + "md5": "^2.3.0", + "minimatch": "^9.0.5", + "nested-property": "^4.0.0", + "node-fetch": "^3.3.2", + "path-posix": "^1.0.0", + "url-join": "^5.0.0", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webdav/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index c76e904..c3dbdd2 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "scripts": { "dev": "astro dev", "start": "astro dev", + "fetch-images": "tsx scripts/fetch-images.ts", + "prebuild": "npm run fetch-images", "build": "astro check && astro build", "preview": "astro preview", "astro": "astro", @@ -15,12 +17,16 @@ "@astrojs/tailwind": "^5.1.0", "@biomejs/biome": "1.7.3", "@tailwindcss/typography": "^0.5.13", + "@types/node": "^25.0.3", "astro": "^4.8.2", + "dotenv": "^17.2.3", "tailwindcss": "^3.4.3", - "typescript": "^5.4.5" + "tsx": "^4.21.0", + "typescript": "^5.4.5", + "webdav": "^5.8.0" }, "dependencies": { "@astrojs/check": "^0.9.2", "typescript": "^5.5.4" } -} \ No newline at end of file +} diff --git a/public/assets/images/favicon.png b/public/assets/images/favicon.png index 151a8b6212e7b834027a69159bbf2022f1df523d..96bdeb14ad5579633bb825538b2238b48c55892e 100644 GIT binary patch literal 2304 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 diff --git a/public/favicon.ico b/public/favicon.ico index 24bcebcdf8fd35ee88a87203f20a9337e78674b9..96bdeb14ad5579633bb825538b2238b48c55892e 100644 GIT binary patch literal 2304 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 diff --git a/scripts/fetch-images.ts b/scripts/fetch-images.ts new file mode 100644 index 0000000..a0ebdaa --- /dev/null +++ b/scripts/fetch-images.ts @@ -0,0 +1,87 @@ +import "dotenv/config"; +import { createClient } from "webdav"; +import { mkdir, writeFile, stat } from "fs/promises"; +import { join, dirname } from "path"; + +interface FileStat { + filename: string; + basename: string; + type: "file" | "directory"; + size: number; + lastmod: string; +} + +const WEBDAV_URL = process.env.WEBDAV_URL || "https://nas.arfaoui.net:6006"; +const WEBDAV_PATH = process.env.WEBDAV_PATH || "/photo/Portfolio"; +const WEBDAV_USER = process.env.WEBDAV_USER; +const WEBDAV_PASS = process.env.WEBDAV_PASS; +const DEST_DIR = "src/assets/images/photos"; + +async function main() { + if (!WEBDAV_USER || !WEBDAV_PASS) { + console.error("Error: WEBDAV_USER and WEBDAV_PASS environment variables are required"); + process.exit(1); + } + + const client = createClient(WEBDAV_URL, { + username: WEBDAV_USER, + password: WEBDAV_PASS, + }); + + console.log(`Fetching images from ${WEBDAV_URL}${WEBDAV_PATH}...`); + + await syncDirectory(client, WEBDAV_PATH, DEST_DIR); + + console.log("Done!"); +} + +async function syncDirectory(client: ReturnType, remotePath: string, localPath: string) { + await mkdir(localPath, { recursive: true }); + + const items = (await client.getDirectoryContents(remotePath)) as FileStat[]; + + for (const item of items) { + const localItemPath = join(localPath, item.basename); + const remoteItemPath = item.filename; + + if (item.type === "directory") { + console.log(` [dir] ${item.basename}/`); + await syncDirectory(client, remoteItemPath, localItemPath); + } else if (item.type === "file" && /\.(jpg|jpeg|png|webp)$/i.test(item.basename)) { + const needsDownload = await shouldDownload(localItemPath, item); + + if (needsDownload) { + console.log(` [download] ${remoteItemPath}`); + const content = (await client.getFileContents(remoteItemPath)) as Buffer; + await mkdir(dirname(localItemPath), { recursive: true }); + await writeFile(localItemPath, content); + } else { + console.log(` [skip] ${item.basename} (unchanged)`); + } + } + } +} + +async function shouldDownload(localPath: string, remoteItem: FileStat): Promise { + try { + const localStat = await stat(localPath); + const remoteSize = remoteItem.size; + const localSize = localStat.size; + + if (remoteSize !== localSize) { + return true; + } + + const remoteDate = new Date(remoteItem.lastmod).getTime(); + const localDate = localStat.mtime.getTime(); + + return remoteDate > localDate; + } catch { + return true; + } +} + +main().catch((err) => { + console.error("Error:", err.message); + process.exit(1); +}); \ No newline at end of file diff --git a/src/collections/.gitkeep b/src/collections/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/collections/experiences.json b/src/collections/experiences.json deleted file mode 100644 index 882242c..0000000 --- a/src/collections/experiences.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "dates": "June 2018 · Present", - "role": "Front-end Engineer", - "company": "Full Truck Alliance", - "description": "Responsible for customer service and CRM system front-end development.", - "logo": "/assets/images/experiences/fta.ico" - }, - { - "dates": "July 2015 · June 2018", - "role": "Front-end Engineer", - "company": "YOHO!", - "description": "Responsible for mobile front-end development of e-commerce platform.", - "logo": "/assets/images/experiences/yoho.ico" - }, - { - "dates": "September 2014 · July 2015 ", - "role": "Node.JS Developer", - "company": "WuLian", - "description": "Intern, involved in the development of Internet of Things cloud systems.", - "logo": "/assets/images/experiences/wulian.ico" - } -] diff --git a/src/collections/menu.json b/src/collections/menu.json deleted file mode 100644 index 688422f..0000000 --- a/src/collections/menu.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "name": "Home", - "url": "/" - }, - { - "name": "Posts", - "url": "/posts" - }, - { - "name": "Projects", - "url": "/projects" - }, - { - "name": "About", - "url": "/about" - } -] diff --git a/src/components/DarkModeToggle.astro b/src/components/DarkModeToggle.astro new file mode 100644 index 0000000..29d2979 --- /dev/null +++ b/src/components/DarkModeToggle.astro @@ -0,0 +1,55 @@ +--- +// DarkModeToggle component - toggles between light and dark mode +--- + + + + \ No newline at end of file diff --git a/src/components/LanguageSwitcher.astro b/src/components/LanguageSwitcher.astro new file mode 100644 index 0000000..09dfa73 --- /dev/null +++ b/src/components/LanguageSwitcher.astro @@ -0,0 +1,75 @@ +--- +// Mapping des URLs entre langues +const translations: Record> = { + '/a-propos': { + fr: '/a-propos', + en: '/en/about', + ar: '/ar/نبذة-عني' + }, + '/en/about': { + fr: '/a-propos', + en: '/en/about', + ar: '/ar/نبذة-عني' + }, + '/ar/نبذة-عني': { + fr: '/a-propos', + en: '/en/about', + ar: '/ar/نبذة-عني' + }, + // Page d'accueil + '/': { + fr: '/', + en: '/en', + ar: '/ar' + }, + '/en': { + fr: '/', + en: '/en', + ar: '/ar' + }, + '/ar': { + fr: '/', + en: '/en', + ar: '/ar' + } +}; + +// Détection de la langue courante +const pathname = Astro.url.pathname.replace(/\/$/, '') || '/'; +const currentLang = pathname.startsWith('/en') ? 'en' : pathname.startsWith('/ar') ? 'ar' : 'fr'; + +// Récupération des liens traduits ou fallback vers les pages d'accueil +const links = translations[pathname] || { + fr: '/', + en: '/en', + ar: '/ar' +}; + +const languages = [ + { code: 'fr', label: 'FR', name: 'Français' }, + { code: 'en', label: 'EN', name: 'English' }, + { code: 'ar', label: 'ع', name: 'العربية' } +]; +--- + +
+ {languages.map((lang, index) => ( + <> + {index > 0 && ·} + {lang.code === currentLang ? ( + + {lang.label} + + ) : ( + + {lang.label} + + )} + + ))} +
\ No newline at end of file diff --git a/src/components/Link.astro b/src/components/Link.astro new file mode 100644 index 0000000..1d93458 --- /dev/null +++ b/src/components/Link.astro @@ -0,0 +1,16 @@ +--- +interface Props { + href: string; + external?: boolean; + class?: string; +} + +const { href, external = false, class: className = '' } = Astro.props; + +const baseClasses = 'text-indigo-600 dark:text-indigo-400 hover:underline'; +const classes = `${baseClasses} ${className}`.trim(); + +const externalProps = external ? { target: '_blank', rel: 'noopener noreferrer' } : {}; +--- + + \ No newline at end of file diff --git a/src/components/footer.astro b/src/components/footer.astro index b6b67a7..f5b5e9d 100644 --- a/src/components/footer.astro +++ b/src/components/footer.astro @@ -1,5 +1,7 @@ --- import Logo from "../components/logo.astro"; +import LanguageSwitcher from "../components/LanguageSwitcher.astro"; +import DarkModeToggle from "../components/DarkModeToggle.astro"; ---
- © {new Date().getFullYear()} Aria + © {new Date().getFullYear()} Jalil Arfaoui

- - - Instagram - + + + + + + - + Instagram + + - - 𝕏 - - + LinkedIn + + - - GitHub - - - - + GitHub + + + + -
+ \ No newline at end of file diff --git a/src/components/header-i18n.astro b/src/components/header-i18n.astro new file mode 100644 index 0000000..15ab810 --- /dev/null +++ b/src/components/header-i18n.astro @@ -0,0 +1,211 @@ +--- +import { mainMenu } from "../data/menu"; +import Logo from "../components/logo.astro"; +import { getLocaleFromUrl } from "../utils/i18n"; + +const currentPath = Astro.url.pathname; +const locale = getLocaleFromUrl(Astro.url) || 'fr'; + +const languageOptions = [ + { code: 'fr', label: 'FR', flag: '🇫🇷' }, + { code: 'en', label: 'EN', flag: '🇬🇧' }, + { code: 'ar', label: 'AR', flag: '🇸🇦', dir: 'rtl' } +]; + +function getLocalizedUrl(url: string, targetLocale: string): string { + if (targetLocale === 'fr') { + return url; + } + return `/${targetLocale}${url}`; +} + +function getCurrentLocaleUrl(targetLocale: string): string { + let path = currentPath; + + if (locale !== 'fr') { + path = path.replace(`/${locale}`, ''); + } + + if (targetLocale === 'fr') { + return path || '/'; + } + + return `/${targetLocale}${path || '/'}`; +} +--- + +
+ \ No newline at end of file diff --git a/src/components/header.astro b/src/components/header.astro index eda214e..fae9e52 100644 --- a/src/components/header.astro +++ b/src/components/header.astro @@ -1,6 +1,27 @@ --- -import menus from "../collections/menu.json"; import Logo from "../components/logo.astro"; + +// Détection de la langue courante +const pathname = Astro.url.pathname; +const currentLang = pathname.startsWith('/en') ? 'en' : pathname.startsWith('/ar') ? 'ar' : 'fr'; + +// Menu localisé +const menus = { + fr: [ + { name: 'Photo', url: '/photo' }, + { name: 'À propos', url: '/a-propos' } + ], + en: [ + { name: 'Photo', url: '/photo' }, + { name: 'About', url: '/en/about' } + ], + ar: [ + { name: 'صور', url: '/photo' }, + { name: 'نبذة عني', url: '/ar/نبذة-عني' } + ] +}; + +const currentMenus = menus[currentLang] || menus.fr; --- @@ -60,57 +81,16 @@ import Logo from "../components/logo.astro"; { - menus.map((menu) => { - return ( - - {menu.name} - - ) - }) + currentMenus.map((menu) => ( + + {menu.name} + + )) } -
-
- - -
- -
diff --git a/src/components/home/projects.astro b/src/components/home/projects.astro index 61658ee..ba129fd 100644 --- a/src/components/home/projects.astro +++ b/src/components/home/projects.astro @@ -1,5 +1,5 @@ --- -import projects from "../../collections/projects.json"; +import projects from "../../data/projects.json"; import Button from "../button.astro"; import Project from "../project.astro"; --- diff --git a/src/components/icons/HomeIcon.astro b/src/components/icons/HomeIcon.astro new file mode 100644 index 0000000..b6ad18a --- /dev/null +++ b/src/components/icons/HomeIcon.astro @@ -0,0 +1,23 @@ +--- +interface Props { + size?: number; + class?: string; +} + +const { size = 20, class: className = '' } = Astro.props; +--- + + + + + diff --git a/src/components/logo.astro b/src/components/logo.astro index 9004af7..f33083f 100644 --- a/src/components/logo.astro +++ b/src/components/logo.astro @@ -1,11 +1,16 @@ - - - - aria - + + +)} diff --git a/src/components/photo/AlbumHeader.astro b/src/components/photo/AlbumHeader.astro new file mode 100644 index 0000000..3d3b1bb --- /dev/null +++ b/src/components/photo/AlbumHeader.astro @@ -0,0 +1,155 @@ +--- +import { Image } from 'astro:assets'; +import ScrollIndicator from './ScrollIndicator.astro'; + +interface Props { + title: string; + description?: string; + date?: Date; + tags?: string[]; + coverImage?: ImageMetadata; + scrollTarget?: string; +} + +const { title, description, date, tags, coverImage, scrollTarget = '.info-section' } = Astro.props; +--- + +{coverImage && ( +
+ {title} +
+
+

{title}

+ {description &&

{description}

} + {date && ( + + )} +
+ +
+
+)} + +{tags && tags.length > 0 && ( +
+
+ {tags.map(tag => ( + {tag} + ))} +
+
+)} + + \ No newline at end of file diff --git a/src/components/photo/CategoryGrid.astro b/src/components/photo/CategoryGrid.astro new file mode 100644 index 0000000..b4a6207 --- /dev/null +++ b/src/components/photo/CategoryGrid.astro @@ -0,0 +1,307 @@ +--- +import CategoryNav from './CategoryNav.astro'; +import ScrollIndicator from './ScrollIndicator.astro'; +import Lightbox from './Lightbox.astro'; +import { Image } from 'astro:assets'; +import { getEntry } from 'astro:content'; + +const { category } = Astro.props; + +// Récupérer les métadonnées de la catégorie +const categoryData = await getEntry('photoCategories', category); + +// Auto-détection des images du dossier de la catégorie +const allImages = import.meta.glob<{ default: ImageMetadata }>('/src/assets/images/photos/categories/**/*.{jpg,jpeg,png,webp}'); + +// Filtrer les images de cette catégorie +const categoryPath = `/src/assets/images/photos/categories/${category}/`; +const categoryImages = Object.entries(allImages) + .filter(([path]) => path.startsWith(categoryPath)) + .sort(([a], [b]) => a.localeCompare(b)); + +// Résoudre les images et générer les métadonnées +const images = await Promise.all( + categoryImages.map(async ([path, loader]) => { + const img = await loader(); + const filename = path.split('/').pop() || ''; + const title = filename + .replace(/\.[^/.]+$/, '') // Enlever l'extension + .replace(/-/g, ' ') // Remplacer les tirets par des espaces + .replace(/_/g, ' '); // Remplacer les underscores par des espaces + return { + src: img.default, + alt: title, + title: title, + path: path + }; + }) +); + +// Données pour la lightbox +const lightboxImages = images.map(img => ({ + src: img.src.src, + alt: img.alt, + title: img.title +})); +--- + + + + + + + + \ No newline at end of file diff --git a/src/components/photo/CategoryNav.astro b/src/components/photo/CategoryNav.astro new file mode 100644 index 0000000..8fa21c1 --- /dev/null +++ b/src/components/photo/CategoryNav.astro @@ -0,0 +1,251 @@ +--- +import { getCollection } from 'astro:content'; +import HomeIcon from '../icons/HomeIcon.astro'; + +const { currentCategory = '', opaque = false } = Astro.props; + +// Récupérer les catégories depuis la collection, triées par order +const photoCategories = await getCollection('photoCategories'); +const sortedCategories = photoCategories.sort((a, b) => (a.data.order || 99) - (b.data.order || 99)); + +// Catégories photos uniquement +const categories = sortedCategories.map(cat => ({ id: cat.id, title: cat.data.title })); +--- + + + + + + \ No newline at end of file diff --git a/src/components/photo/Lightbox.astro b/src/components/photo/Lightbox.astro new file mode 100644 index 0000000..f10d979 --- /dev/null +++ b/src/components/photo/Lightbox.astro @@ -0,0 +1,329 @@ +--- +import { getCollection } from 'astro:content'; + +interface Props { + images: { src: string; alt: string; title?: string }[]; + albumTitle?: string; + showCategory?: boolean; + category?: string; +} + +const { images, albumTitle = '', showCategory = false, category = '' } = Astro.props; + +const imagesForJS = JSON.stringify(images); + +// Construire les labels depuis la collection +const photoCategories = await getCollection('photoCategories'); +const categoryLabels: Record = { + 'blog': 'Fil Photo', + ...Object.fromEntries(photoCategories.map(cat => [cat.id, cat.data.title])) +}; +--- + + + + + + + + diff --git a/src/components/photo/MasonryGallery.astro b/src/components/photo/MasonryGallery.astro new file mode 100644 index 0000000..8698a4e --- /dev/null +++ b/src/components/photo/MasonryGallery.astro @@ -0,0 +1,78 @@ +--- +import { Image } from 'astro:assets'; + +interface Props { + images: { + src: ImageMetadata; + alt: string; + }[]; + columns?: number; +} + +const { images, columns = 5 } = Astro.props; +--- + + + + \ No newline at end of file diff --git a/src/components/photo/PhotoGallery.astro b/src/components/photo/PhotoGallery.astro new file mode 100644 index 0000000..a283251 --- /dev/null +++ b/src/components/photo/PhotoGallery.astro @@ -0,0 +1,255 @@ +--- +import CategoryNav from './CategoryNav.astro'; +import Slideshow from './Slideshow.astro'; +import SlideControls from './SlideControls.astro'; +import favorites from '../../data/favorites.json'; + +// Auto-détection des images +const allImages = import.meta.glob<{ default: ImageMetadata }>('/src/assets/images/photos/categories/**/*.{jpg,jpeg,png,webp}'); + +// Charger les images favorites +const favoriteImages = await Promise.all( + favorites.map(async (relativePath: string) => { + const fullPath = `/src/assets/images/photos/categories/${relativePath}`; + const loader = allImages[fullPath]; + if (!loader) { + console.warn(`Image favorite non trouvée: ${fullPath}`); + return null; + } + const img = await loader(); + const filename = relativePath.split('/').pop() || ''; + const title = filename + .replace(/\.[^/.]+$/, '') + .replace(/-/g, ' ') + .replace(/_/g, ' '); + return { + src: img.default, // ImageMetadata pour + alt: title, + title: title, + category: relativePath.split('/')[0] + }; + }) +); + +// Filtrer les nulls (images non trouvées) +const images = favoriteImages.filter(img => img !== null); + +// Préparation des données pour JavaScript (avec URL au lieu de ImageMetadata) +const imagesForJS = JSON.stringify(images.map(img => ({ + src: img.src.src, // Extraire l'URL de ImageMetadata + alt: img.alt, + title: img.title, + category: img.category +}))); +--- + + + + + + \ No newline at end of file diff --git a/src/components/photo/ScrollIndicator.astro b/src/components/photo/ScrollIndicator.astro new file mode 100644 index 0000000..2f220af --- /dev/null +++ b/src/components/photo/ScrollIndicator.astro @@ -0,0 +1,79 @@ +--- +const { targetSelector = '.info-section' } = Astro.props; +--- + +
+
+ + + +
+ + + + \ No newline at end of file diff --git a/src/components/photo/SlideControls.astro b/src/components/photo/SlideControls.astro new file mode 100644 index 0000000..edbac9a --- /dev/null +++ b/src/components/photo/SlideControls.astro @@ -0,0 +1,92 @@ +--- +--- + +
+ + + +
+ + \ No newline at end of file diff --git a/src/components/photo/Slideshow.astro b/src/components/photo/Slideshow.astro new file mode 100644 index 0000000..20120c6 --- /dev/null +++ b/src/components/photo/Slideshow.astro @@ -0,0 +1,141 @@ +--- +import { Image } from 'astro:assets'; + +const { images = [] } = Astro.props; +--- + +
+
+ {images.map((image, index) => ( +
+ {image.alt} +
+ ))} +
+ + +
+ {images.map((_, index) => ( +
+
+ + \ No newline at end of file diff --git a/src/content/config.js b/src/content/config.js deleted file mode 100644 index a8ec09b..0000000 --- a/src/content/config.js +++ /dev/null @@ -1,14 +0,0 @@ -import { defineCollection, z } from "astro:content"; - -const postCollection = defineCollection({ - type: "content", - schema: z.object({ - title: z.string(), - description: z.string(), - dateFormatted: z.string(), - }), -}); - -export const collections = { - post: postCollection, -}; diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..d010223 --- /dev/null +++ b/src/content/config.ts @@ -0,0 +1,85 @@ +import { defineCollection, z, type ImageFunction } from "astro:content"; + +const blogCollection = defineCollection({ + type: "content", + schema: z.object({ + title: z.string(), + description: z.string(), + date: z.date(), + dateFormatted: z.string(), + category: z.enum(['pro', 'comedy', 'photo']), + tags: z.array(z.string()).optional(), + image: z.string().optional(), + imageAlt: z.string().optional(), + draft: z.boolean().default(false), + lang: z.enum(['fr', 'en', 'ar']).default('fr'), + }), +}); + +const projectsCollection = defineCollection({ + type: "content", + schema: z.object({ + title: z.string(), + description: z.string(), + date: z.date(), + dateFormatted: z.string(), + category: z.enum(['dev', 'comedy', 'photo']), + technologies: z.array(z.string()).optional(), + url: z.string().url().optional(), + github: z.string().url().optional(), + image: z.string().optional(), + imageAlt: z.string().optional(), + featured: z.boolean().default(false), + draft: z.boolean().default(false), + lang: z.enum(['fr', 'en', 'ar']).default('fr'), + }), +}); + +const talksCollection = defineCollection({ + type: "content", + schema: z.object({ + title: z.string(), + description: z.string(), + date: z.date(), + dateFormatted: z.string(), + event: z.string(), + location: z.string(), + slides: z.string().url().optional(), + video: z.string().url().optional(), + image: z.string().optional(), + imageAlt: z.string().optional(), + tags: z.array(z.string()).optional(), + draft: z.boolean().default(false), + lang: z.enum(['fr', 'en', 'ar']).default('fr'), + }), +}); + +const photoBlogPostsCollection = defineCollection({ + type: "content", + schema: ({ image }) => z.object({ + title: z.string(), + description: z.string(), + date: z.date(), + coverImage: image(), + tags: z.array(z.string()).optional(), + featured: z.boolean().default(false), + draft: z.boolean().default(false), + }), +}); + +const photoCategoriesCollection = defineCollection({ + type: "data", + schema: z.object({ + title: z.string(), + subtitle: z.string(), + order: z.number().optional(), + }), +}); + +export const collections = { + blog: blogCollection, + projects: projectsCollection, + talks: talksCollection, + photoBlogPosts: photoBlogPostsCollection, + photoCategories: photoCategoriesCollection, +}; \ No newline at end of file diff --git a/src/content/photoBlogPosts/enigma.md b/src/content/photoBlogPosts/enigma.md new file mode 100644 index 0000000..9f2c9ae --- /dev/null +++ b/src/content/photoBlogPosts/enigma.md @@ -0,0 +1,15 @@ +--- +title: "Enigma" +description: "Série artistique en noir et blanc" +date: 2015-04-25 +coverImage: "../../assets/images/photos/blog/enigma/01-Enigma-v1.jpg" +tags: [] +featured: true +draft: false +--- + +# Enigma + +Série artistique en noir et blanc + +**40 photos** diff --git a/src/content/photoBlogPosts/eroll.md b/src/content/photoBlogPosts/eroll.md new file mode 100644 index 0000000..f7d3806 --- /dev/null +++ b/src/content/photoBlogPosts/eroll.md @@ -0,0 +1,15 @@ +--- +title: "Shooting Eroll" +description: "Séance photo portrait et sport avec Eroll" +date: 2011-10-02 +coverImage: "../../assets/images/photos/blog/eroll/01-Eroll-Shooting-1.jpg" +tags: [] +featured: false +draft: false +--- + +# Shooting Eroll + +Séance photo portrait et sport avec Eroll + +**17 photos** diff --git a/src/content/photoBlogPosts/field-of-stones.md b/src/content/photoBlogPosts/field-of-stones.md new file mode 100644 index 0000000..280acc2 --- /dev/null +++ b/src/content/photoBlogPosts/field-of-stones.md @@ -0,0 +1,15 @@ +--- +title: "Field of Stones" +description: "Shooting avec Marco Wolter" +date: 2015-04-02 +coverImage: "../../assets/images/photos/blog/field-of-stones/01-Marco-Wolter-Field-of-Stones-2.jpg" +tags: [] +featured: false +draft: false +--- + +# Field of Stones + +Shooting avec Marco Wolter + +**11 photos** diff --git a/src/content/photoBlogPosts/helsinki.md b/src/content/photoBlogPosts/helsinki.md new file mode 100644 index 0000000..0d64c91 --- /dev/null +++ b/src/content/photoBlogPosts/helsinki.md @@ -0,0 +1,15 @@ +--- +title: "Helsinki" +description: "Découverte de la capitale finlandaise" +date: 2013-05-15 +coverImage: "../../assets/images/photos/blog/helsinki/01-Library-of-University-of-Helsinki.jpg" +tags: [] +featured: false +draft: false +--- + +# Helsinki + +Découverte de la capitale finlandaise + +**14 photos** diff --git a/src/content/photoBlogPosts/ifrane-hike.md b/src/content/photoBlogPosts/ifrane-hike.md new file mode 100644 index 0000000..1ca796e --- /dev/null +++ b/src/content/photoBlogPosts/ifrane-hike.md @@ -0,0 +1,15 @@ +--- +title: "Randonnée à Ifrane" +description: "Randonnée hivernale dans les montagnes du Moyen Atlas" +date: 2013-01-13 +coverImage: "../../assets/images/photos/blog/ifrane-hike/01-.jpg" +tags: [] +featured: false +draft: false +--- + +# Randonnée à Ifrane + +Randonnée hivernale dans les montagnes du Moyen Atlas + +**9 photos** diff --git a/src/content/photoBlogPosts/inox-park-2011.md b/src/content/photoBlogPosts/inox-park-2011.md new file mode 100644 index 0000000..5ee9e38 --- /dev/null +++ b/src/content/photoBlogPosts/inox-park-2011.md @@ -0,0 +1,15 @@ +--- +title: "Inox Park Paris 2011" +description: "Festival de musique électronique à Chatou" +date: 2011-09-10 +coverImage: "../../assets/images/photos/blog/inox-park-2011/01-Inox-Park-Paris-Chatou-2011.jpg" +tags: [] +featured: false +draft: false +--- + +# Inox Park Paris 2011 + +Festival de musique électronique à Chatou + +**18 photos** diff --git a/src/content/photoBlogPosts/london-calling.md b/src/content/photoBlogPosts/london-calling.md new file mode 100644 index 0000000..b3bde7d --- /dev/null +++ b/src/content/photoBlogPosts/london-calling.md @@ -0,0 +1,15 @@ +--- +title: "London Calling" +description: "Week-end photographique à Londres" +date: 2014-07-15 +coverImage: "../../assets/images/photos/blog/london-calling/01-The-sky-inside.jpg" +tags: [] +featured: false +draft: false +--- + +# London Calling + +Week-end photographique à Londres + +**7 photos** diff --git a/src/content/photoBlogPosts/no-wind-las-cuevas.md b/src/content/photoBlogPosts/no-wind-las-cuevas.md new file mode 100644 index 0000000..80fce25 --- /dev/null +++ b/src/content/photoBlogPosts/no-wind-las-cuevas.md @@ -0,0 +1,15 @@ +--- +title: "Pas de vent à Las Cuevas" +description: "Journée kitesurf à Tarifa" +date: 2015-01-10 +coverImage: "../../assets/images/photos/blog/no-wind-las-cuevas/01-No-wind-at-Las-Cuevas.jpg" +tags: [] +featured: false +draft: false +--- + +# Pas de vent à Las Cuevas + +Journée kitesurf à Tarifa + +**8 photos** diff --git a/src/content/photoBlogPosts/schoolbag-operation-2012.md b/src/content/photoBlogPosts/schoolbag-operation-2012.md new file mode 100644 index 0000000..453336f --- /dev/null +++ b/src/content/photoBlogPosts/schoolbag-operation-2012.md @@ -0,0 +1,15 @@ +--- +title: "Opération Cartable 2012" +description: "Distribution de cartables par la JCI Tanger" +date: 2012-09-30 +coverImage: "../../assets/images/photos/blog/schoolbag-operation-2012/01-.jpg" +tags: [] +featured: false +draft: false +--- + +# Opération Cartable 2012 + +Distribution de cartables par la JCI Tanger + +**51 photos** diff --git a/src/content/photoBlogPosts/sequanian-sunday.md b/src/content/photoBlogPosts/sequanian-sunday.md new file mode 100644 index 0000000..65c497b --- /dev/null +++ b/src/content/photoBlogPosts/sequanian-sunday.md @@ -0,0 +1,15 @@ +--- +title: "Dimanche Séquanais" +description: "Balade dominicale le long de la Seine" +date: 2014-05-18 +coverImage: "../../assets/images/photos/blog/sequanian-sunday/01-Court-of-Audit-Paris.jpg" +tags: [] +featured: false +draft: false +--- + +# Dimanche Séquanais + +Balade dominicale le long de la Seine + +**10 photos** diff --git a/src/content/photoBlogPosts/tangier-walk.md b/src/content/photoBlogPosts/tangier-walk.md new file mode 100644 index 0000000..2721fbe --- /dev/null +++ b/src/content/photoBlogPosts/tangier-walk.md @@ -0,0 +1,15 @@ +--- +title: "Balade à Tanger" +description: "Promenade photographique dans les rues de Tanger" +date: 2012-05-26 +coverImage: "../../assets/images/photos/blog/tangier-walk/01-Observer-le-changement.jpg" +tags: [] +featured: false +draft: false +--- + +# Balade à Tanger + +Promenade photographique dans les rues de Tanger + +**9 photos** diff --git a/src/content/photoBlogPosts/waiting-for-the-bride.md b/src/content/photoBlogPosts/waiting-for-the-bride.md new file mode 100644 index 0000000..7155f18 --- /dev/null +++ b/src/content/photoBlogPosts/waiting-for-the-bride.md @@ -0,0 +1,15 @@ +--- +title: "En attendant la mariée" +description: "Préparatifs d'un mariage" +date: 2014-10-25 +coverImage: "../../assets/images/photos/blog/waiting-for-the-bride/01-.jpg" +tags: [] +featured: false +draft: false +--- + +# En attendant la mariée + +Préparatifs d'un mariage + +**3 photos** diff --git a/src/content/photoBlogPosts/wandering-tangier-medina.md b/src/content/photoBlogPosts/wandering-tangier-medina.md new file mode 100644 index 0000000..73c660c --- /dev/null +++ b/src/content/photoBlogPosts/wandering-tangier-medina.md @@ -0,0 +1,15 @@ +--- +title: "Dans la médina de Tanger" +description: "Flânerie dans les ruelles de la médina" +date: 2014-08-10 +coverImage: "../../assets/images/photos/blog/wandering-tangier-medina/01-The-watchmaker.jpg" +tags: [] +featured: false +draft: false +--- + +# Dans la médina de Tanger + +Flânerie dans les ruelles de la médina + +**10 photos** diff --git a/src/content/photoBlogPosts/wedding-aurore-thomas.md b/src/content/photoBlogPosts/wedding-aurore-thomas.md new file mode 100644 index 0000000..7905320 --- /dev/null +++ b/src/content/photoBlogPosts/wedding-aurore-thomas.md @@ -0,0 +1,11 @@ +--- +title: "Mariage Aurore & Thomas" +description: "Reportage de mariage" +date: 2015-09-26 +coverImage: "../../assets/images/photos/blog/wedding-aurore-thomas/01-Mariage-Aurore-Thomas.jpg" +tags: [] +featured: true +draft: false +--- + +**13 photos** diff --git a/src/content/photoCategories/cultures.json b/src/content/photoCategories/cultures.json new file mode 100644 index 0000000..26630aa --- /dev/null +++ b/src/content/photoCategories/cultures.json @@ -0,0 +1,5 @@ +{ + "title": "Cultures et Traditions", + "subtitle": "Richesse des traditions humaines", + "order": 4 +} diff --git a/src/content/photoCategories/engines.json b/src/content/photoCategories/engines.json new file mode 100644 index 0000000..eba8527 --- /dev/null +++ b/src/content/photoCategories/engines.json @@ -0,0 +1,5 @@ +{ + "title": "Moteurs", + "subtitle": "Mécanique et puissance en mouvement", + "order": 7 +} diff --git a/src/content/photoCategories/everyday.json b/src/content/photoCategories/everyday.json new file mode 100644 index 0000000..9133fb8 --- /dev/null +++ b/src/content/photoCategories/everyday.json @@ -0,0 +1,5 @@ +{ + "title": "Quotidien", + "subtitle": "Instants du quotidien", + "order": 8 +} diff --git a/src/content/photoCategories/music.json b/src/content/photoCategories/music.json new file mode 100644 index 0000000..809affd --- /dev/null +++ b/src/content/photoCategories/music.json @@ -0,0 +1,5 @@ +{ + "title": "Musique et Célébrations", + "subtitle": "Notes, chansons et autres vibrations qui célèbrent les petits et grands moments de nos vies", + "order": 5 +} diff --git a/src/content/photoCategories/nature.json b/src/content/photoCategories/nature.json new file mode 100644 index 0000000..58e4dfd --- /dev/null +++ b/src/content/photoCategories/nature.json @@ -0,0 +1,5 @@ +{ + "title": "Nature", + "subtitle": "La magie du monde naturel", + "order": 3 +} diff --git a/src/content/photoCategories/places.json b/src/content/photoCategories/places.json new file mode 100644 index 0000000..a57b863 --- /dev/null +++ b/src/content/photoCategories/places.json @@ -0,0 +1,5 @@ +{ + "title": "Paysages", + "subtitle": "Beauté des paysages et des lieux", + "order": 2 +} diff --git a/src/content/photoCategories/portraits.json b/src/content/photoCategories/portraits.json new file mode 100644 index 0000000..3bafddf --- /dev/null +++ b/src/content/photoCategories/portraits.json @@ -0,0 +1,5 @@ +{ + "title": "Portraits", + "subtitle": "Expressions et émotions capturées", + "order": 1 +} diff --git a/src/content/photoCategories/sports.json b/src/content/photoCategories/sports.json new file mode 100644 index 0000000..7eec454 --- /dev/null +++ b/src/content/photoCategories/sports.json @@ -0,0 +1,5 @@ +{ + "title": "Sports", + "subtitle": "Mouvement, effort et dépassement", + "order": 6 +} diff --git a/src/content/post/ai-remove-image-background.md b/src/content/post/ai-remove-image-background.md deleted file mode 100644 index 090ffff..0000000 --- a/src/content/post/ai-remove-image-background.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Browser locally uses AI to remove image backgrounds -description: Browser locally uses AI to remove image backgrounds -dateFormatted: Jul 14th, 2024 ---- - -Yo, so I've been digging into this whole AI thing for front-end development lately, and stumbled upon this cool Transformers.js example. Turned it into a sweet little tool, check it out! - -Basically, it uses Transformers.js in a WebWorker to tap into WebGPU and run this RMBG-1.4 model. Long story short, you can now use AI to nuke image backgrounds right in your browser. And get this, it only takes half a second to process a 4K image on my M1 PRO! - -Here's the link to the tool: [https://html.zone/background-remover](https://html.zone/background-remover) - -[![AI background remover](https://og-image.html.zone/https://html.zone/background-remover)](https://html.zone/background-remover) - -* * * - -Wanna build it yourself? Head over to [https://github.com/xenova/transformers.js/tree/main/examples/remove-background-client](https://github.com/xenova/transformers.js/tree/main/examples/remove-background-client) for the source code. Oh, and heads up, you gotta be on Transformers.js V3 to mess with WebGPU. diff --git a/src/content/post/astro-aria.md b/src/content/post/astro-aria.md deleted file mode 100644 index 8bdfcf0..0000000 --- a/src/content/post/astro-aria.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Aria - a minimalist Astro homepage template -description: Aria is a template for Astro -dateFormatted: Jun 6th, 2024 ---- - -[![GitHub](https://github.html.zone/ccbikai/astro-aria)](https://github.com/ccbikai/astro-aria) - -Aria is a template I found on [https://aria.devdojo.io/](https://aria.devdojo.io/). It's clean and beautiful, so I decided to use it for my own homepage and ported it to Astro. - -It's already open source, so feel free to use it if you're interested. - - diff --git a/src/content/post/broadcast-channel.md b/src/content/post/broadcast-channel.md deleted file mode 100644 index 50fbc08..0000000 --- a/src/content/post/broadcast-channel.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: BroadcastChannel - Turn your Telegram Channel into a MicroBlog -description: Turn your Telegram Channel into a MicroBlog -dateFormatted: Aug 11th, 2024 ---- - -I have been sharing some interesting tools on [X](https://x.com/ccbikai) and also synchronizing them to my Telegram Channel. I saw that [Austin mentioned he is preparing to create a website](https://x.com/austinit/status/1817832660758081651) to compile all the shared content. This reminded me of a template I recently came across called [Sepia](https://github.com/Planetable/SiteTemplateSepia), and I thought about converting the Telegram Channel into a microblog. - -The difficulty wasn't high; I completed the main functionality over a weekend. During the process, I achieved a browser-side implementation with zero JavaScript and would like to share some interesting technical points: - -1. The anti-spoiler mode and the hidden display of the mobile search box were implemented using the CSS ":checked pseudo-class" and the "+ adjacent sibling combinator." [Reference](https://www.tpisoftware.com/tpu/articleDetails/2744) - -2. The transition animations utilized CSS View Transitions. [Reference](https://liruifengv.com/posts/zero-js-view-transitions/) - -3. The image lightbox used the HTML popover attribute. [Reference](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/popover) - -4. The display and hiding of the "back to top" feature were implemented using CSS animation-timeline, exclusive to Chrome version 115 and above. [Reference](https://developer.mozilla.org/zh-CN/docs/Web/CSS/animation-timeline/view) - -5. The multi-image masonry layout was achieved using grid layout. [Reference](https://www.smashingmagazine.com/native-css-masonry-layout-css-grid/) - -6. The visit statistics were tracked using a 1px transparent image as the logo background, an ancient technique that is now rarely supported by visit statistics software. - -7. JavaScript execution on the browser side was prohibited using the Content-Security-Policy's script-src 'none'. [Reference](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) - -After completing the project, I open-sourced it, and I was pleasantly surprised by the number of people who liked it; I received over 800 stars in just a week. - -If you're interested, you can check it out on GitHub. - - - -[![BroadcastChannel repository on GitHub](https://github.html.zone/ccbikai/BroadcastChannel)](https://github.com/ccbikai/BroadcastChannel) diff --git a/src/content/post/cloudflare-web-analytics-kill-adblock.md b/src/content/post/cloudflare-web-analytics-kill-adblock.md deleted file mode 100644 index 5f3936b..0000000 --- a/src/content/post/cloudflare-web-analytics-kill-adblock.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Solving the issue of Cloudflare Web Analytics being blocked by AdBlock -description: Solving the issue of Cloudflare Web Analytics being blocked by AdBlock -dateFormatted: Jan 8th, 2024 ---- - -Earlier, we solved the issues of [Vercel Analytics](https://dev.to/ccbikai/jie-jue-vercel-analytics-bei-adblock-ping-bi-wen-ti-1o21-temp-slug-5601874) and [Umami](https://dev.to/ccbikai/jie-jue-umami-bei-adblock-ping-bi-wen-ti-3kc2-temp-slug-2355567) being blocked by AdBlock, and now we are also going to solve the problem for [Email.ML](https://email.ml/) which uses [Cloudflare Web Analytics](https://www.cloudflare.com/zh-cn/web-analytics/). - -Cloudflare Web Analytics is blocked by the `||cloudflareinsights.com^` rule. Its script address is `https://static.cloudflareinsights.com/beacon.min.js`, and the data reporting address is `https://cloudflareinsights.com/cdn-cgi/rum`. - -![||cloudflareinsights.com^](https://static.miantiao.me/share/2024/U4WHW7/GtPNhj.png) - -So, just like Umami, we will proxy the script address and forward the data to the data reporting address. - -## Solution - -Create a Worker in Cloudflare Workers and paste the following JavaScript code. Configure the domain and test if the script address can be accessed properly. Mine is [https://cwa.miantiao.me/mt-demo.js](https://cwa.miantiao.me/mt-demo.js). The `mt-demo` can be replaced with any disguise address, the script above is already adapted. - -```js -const CWA_API = 'https://cloudflareinsights.com/cdn-cgi/rum' -const CWA_SCRIPT = 'https://static.cloudflareinsights.com/beacon.min.js' - -export default { - async fetch(request, env, ctx) { - let { pathname, search } = new URL(request.url) - if (pathname.endsWith('.js')) { - let response = await caches.default.match(request) - if (!response) { - response = await fetch(CWA_SCRIPT, request) - ctx.waitUntil(caches.default.put(request, response.clone())) - } - return response - } - const req = new Request(request) - req.headers.delete("cookie") - const response = await fetch(`${CWA_API}${search}`, req) - const headers = Object.fromEntries(response.headers.entries()) - if (!response.headers.has('Access-Control-Allow-Origin')) { - headers['Access-Control-Allow-Origin'] = request.headers.get('Origin') || '*' - } - if (!response.headers.has('Access-Control-Allow-Headers')) { - headers['Access-Control-Allow-Headers'] = 'content-type' - } - if (!response.headers.has('Access-Control-Allow-Credentials')) { - headers['Access-Control-Allow-Credentials'] = 'true' - } - return new Response(response.body, { - status: response.status, - headers - }) - }, -}; - -``` - -Then inject the script into your website project, referring to my code: - -```html - - -``` - -`src` is the script address, replace `mt-demo` with any disguise address. `data-cf-beacon` contains the send to data reporting address, replace `mt-demo` with any disguise address, the script is already adapted. Remember to change the `token` to your site's token. - -You can verify it on [Email.ML](https://email.ml/) or [HTML.ZONE](https://html.zone/). - -**Note that using this solution requires disabling automatic configuration, otherwise the data will not be counted.** - -![Disable automatic configuration](https://static.miantiao.me/share/2024/AnFeat/jqthrz.png) \ No newline at end of file diff --git a/src/content/post/cloudflare-worker-image.md b/src/content/post/cloudflare-worker-image.md deleted file mode 100644 index 34e038f..0000000 --- a/src/content/post/cloudflare-worker-image.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Processing Images with Cloudflare Worker -description: Processing Images with Cloudflare Worker -dateFormatted: Nov 18th, 2023 ---- - -## Background - -Previously, I set up a 10GB storage, unlimited bandwidth cloud storage using [Backblaze B2](https://www.backblaze.com/cloud-storage) and Cloudflare, which I use for daily file sharing and as an image hosting service for my blog. It works well with uPic. However, when using it as an image hosting service for my blog, I found that it doesn't support image resizing/cropping. I often use Alibaba Cloud OSS for image processing at work, and I couldn't stand the limitation, so I decided to create my own service. - -> The free version of Workers only has a CPU limit of 10ms, and it frequently exceeds the resource usage limit, resulting in a high rate of image cracking. Now it has been adapted to Vercel Edge, which can be used with a CDN. See [https://chi.miantiao.me/post/cloudflare-worker-image/](https://chi.miantiao.me/post/cloudflare-worker-image/) - -## Process - -After some research, I considered two options: - -1. Use Cloudflare to proxy [Vercel Image](https://vercel.com/docs/image-optimization). With this option, the traffic goes through Cloudflare -> Vercel -> Cloudflare -> Backblaze, which is not ideal in terms of stability and speed. Additionally, it only allows 1000 image processing requests per month, which is quite limited. - -2. Use the public service [wsrv.nl](https://images.weserv.nl/). With this option, the traffic goes through Cloudflare -> wsrv.nl -> Cloudflare -> Backblaze, and the domain is not under my control. If I want to control the domain, I would have to go through Cloudflare Worker again, which adds complexity. - -Since neither option was ideal, I kept looking for alternatives. Last week, when I was working on an Email Worker, I discovered that Cloudflare Worker supports [WebAssembly (Wasm)](https://developers.cloudflare.com/workers/runtime-apis/webassembly/), which sparked the idea of using Worker + WebAssembly to process images. - -Initially, I wanted to use [sharp](https://sharp.pixelplumbing.com/), which I had used when working with Node.js. However, the author mentioned that Cloudflare Worker does not support multithreading, so sharp cannot run on Cloudflare Worker in the short term. - -I searched online and found that a popular Rust library for image processing is [Photon](https://silvia-odwyer.github.io/photon/), and there is also a [demo](https://github.com/techwithdeo/cloudflare-workers/tree/main/photon-library) in the community. I tried it out and confirmed that it can run on Cloudflare Worker. However, the demo has two drawbacks: - -1. Photon needs to be manually updated and cannot keep up with the official updates as quickly. -2. It can only output images in PNG format, and the file size of JPG images actually becomes larger after resizing. - -## Result - -Based on the keywords "Photon + Worker", I did further research and came up with a new solution inspired by [DenoFlare](https://denoflare.dev/examples/transform-images-wasm) and [jSquash](https://github.com/jamsinclair/jSquash). In the end, I used the official Photon (with patch-package as a dependency), Squash WebAssembly, and Cloudflare Worker to create an image processing service for resizing images. _I originally wanted to support output in AVIF and JPEG XL formats, but due to the 1MB size limit of the free version of Workers, I had to give up this feature_. - -Supported features: - -1. Supports processing of PNG, JPG, BMP, ICO, and TIFF format images. -2. Can output images in JPG, PNG, and WEBP formats, with WEBP being the default. -3. Supports pipelining, allowing multiple operations to be executed. -4. Supports Cloudflare caching. -5. Supports whitelisting of image URLs to prevent abuse. -6. Degrades gracefully in case of exceptions, returning the original image (exceptions are not cached). - -## Demo - -### Format Conversion - -#### webp - -![webp](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&format=webp) - -#### jpg - -![jpg](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&format=jpg) - -#### png - -![png](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&format=png) - -### Resizing - -![resize](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=resize!830,400,2) - -### Rotation - -![rotate](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=rotate!90) - -### Cropping - -![rotate](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=crop!0,0,1000,1000) - -### Filters - -![filter](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=filter%21obsidian) - -### Image Watermark - -![watermark](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=watermark!https%3A%2F%2Fstatic.miantiao.me%2Fshare%2F6qIq4w%2FFhSUzU.png,20,20) - -### Text Watermark - -![draw_text](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=draw_text!miantiao.me,20,20) - -### Pipeline Operations - -#### Resize + Rotate + Text Watermark - -![resize & rotate & draw_text](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=resize!830,400,2%7Crotate!180%7Cdraw_text!miantiao.me,10,10) - -#### Resize + Image Watermark - -![resize & watermark](https://image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=resize!830,400,2%7Cwatermark!https%3A%2F%2Fstatic.miantiao.me%2Fshare%2F6qIq4w%2FFhSUzU.png,10,10) - -In theory, it supports all the operations of Photon. If you are interested, you can check the image URLs and modify the parameters according to the [Photon documentation](https://docs.rs/photon-rs/latest/photon_rs/) to try it out yourself. If you encounter any issues, feel free to leave a comment and provide feedback. - -## Sharing - -I have open-sourced this solution on my GitHub. If you need it, you can follow the documentation to deploy it. - -[![ccbikai/cloudflare-worker-image - GitHub](https://github.html.zone/ccbikai/cloudflare-worker-image)](https://github.com/ccbikai/cloudflare-worker-image) - -* * * - -[![Buy Me A Coffee](https://static.miantiao.me/share/0WmsVP/CcmGr8.png)](https://www.buymeacoffee.com/ccbikai) diff --git a/src/content/post/deploy-fediverse-instance.md b/src/content/post/deploy-fediverse-instance.md deleted file mode 100644 index 9a473d9..0000000 --- a/src/content/post/deploy-fediverse-instance.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Low-Cost Deployment of Federated Universe Personal Instances -description: Low-Cost Deployment of Federated Universe Personal Instances -dateFormatted: Nov 27th, 2023 ---- - -I came across the concept of the Fediverse at the beginning of this year and found that it is the social network I have always envisioned: each instance is like an isolated island, connected through the network to communicate with each other. - -> To learn more about the Fediverse, you can read the blog posts from these individuals: -> -> - [Introduction to the Fediverse](https://zerovip.vercel.app/zh/59563/) -> - [Fediverse: The Federated Universe](https://wzyboy.im/post/1486.html) -> - [What is the Fediverse and Can It Decentralize the Internet?](https://fermi.ink/posts/2022/11/22/01/) -> - [What is Mastodon and How to Use It](https://limboy.me/posts/mastodon/) -> - [Fediverse Guide for Twitter Users](https://wzyboy.im/post/1513.html) - -As a self-hosting enthusiast, I wanted to deploy my own instance. I asked about the cost of self-hosting on Mastodon and found that the minimum cost is $15/year for a server and domain name. In order to reduce costs, I didn't purchase a VPS and instead deployed my own instance on my Homelab. It has been running for half a year with a few issues (mainly due to my tinkering) such as internet or power outages at home. Since downtime results in lost messages, I decided to migrate to a server. - -Among the popular software, Mastodon has more features but consumes more resources, so I chose [Pleroma](https://pleroma.social/) which consumes fewer resources but still meets my needs. I deployed it on various free services, achieving a server cost of $0 with only the domain name cost remaining. It has been running stable for a quarter. - -![chi@miantiao.me](https://static.miantiao.me/share/nNbzS2/miantiao.me_chi.jpg) - -Therefore, I would like to share this solution: - -- Cloud platforms: - 1. [Koyeb](https://app.koyeb.com/) - 2. [Northflank](https://northflank.com/) - 3. [Zeabur](https://s.mt.ci/WrK7Dc) (Originally free, but now only available through subscription plans (free plan is for testing only)) - -- Database: - 1. [Aiven](https://s.mt.ci/dgQGhM) - 2. [Neon](https://neon.tech/) - -- Cloud storage: - 1. [Cloudflare R2](https://www.cloudflare.com/zh-cn/developer-platform/r2/) - 2. [Backblaze B2](https://www.backblaze.com/) - -- CDN: - 1. [Cloudflare](https://www.cloudflare.com/) - -Deployment tutorial: - -[![ccbikai/pleroma-on-cloud - GitHub](https://github.html.zone/ccbikai/pleroma-on-cloud)](https://github.com/ccbikai/pleroma-on-cloud) - -Remember, free things are often the most expensive. It is important to regularly back up the database and cloud storage. - -**Lastly, feel free to follow me on the Fediverse (Mastodon, Pleroma, etc.) at [@chi@miantiao.me](https://miantiao.me/@chi).** diff --git a/src/content/post/dns-surf.md b/src/content/post/dns-surf.md deleted file mode 100644 index 72066e9..0000000 --- a/src/content/post/dns-surf.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: DNS.Surf - check DNS resolution results in different regions -description: DNS.Surf - check DNS resolution results in different regions -dateFormatted: Nov 8th, 2023 ---- - - -[**DNS.Surf**](https://dns.surf/) is like a traveler that helps you explore the scenery of DNS resolution results in different regions. - -It provides resolution services from 18 regions and has over 100 optional DNS resolvers, just like choosing how to travel between different cities and countries. - -This website runs entirely on Vercel, like a stable and efficient means of transportation, providing you with fast and reliable service. - -## Privacy - -For privacy concerns, you can use it with confidence, as the website does not collect or store any user information. It's like enjoying the scenery during your travels without worrying about personal information leakage. - -## Website - -[https://dns.surf/](https://dns.surf/) diff --git a/src/content/post/email-ml.md b/src/content/post/email-ml.md deleted file mode 100644 index 7e24550..0000000 --- a/src/content/post/email-ml.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Email.ML - minimalistic temporary email -description: Email.ML - minimalistic temporary email -dateFormatted: Jun 6th, 2024 ---- - -[**Email.ML**](https://email.ml/) is a minimalistic temporary email service. - -You can get a temporary email without revealing any personal information, which greatly protects your privacy. - -It supports selecting multiple domain names, making it convenient for you to use in different scenarios. - -100% running on the **Cloudflare** network, providing you with a super-fast experience. - -## Statement - -This service is not available in China Mainland. - -## Privacy - -This site only stores an email name for this session, and the emails are temporarily stored in **Cloudflare** data centers. They will be completely deleted after the email expires. - -## Website - -[https://email.ml/](https://email.ml/) \ No newline at end of file diff --git a/src/content/post/github-og-image.md b/src/content/post/github-og-image.md deleted file mode 100644 index 00922f1..0000000 --- a/src/content/post/github-og-image.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Extract GitHub OpenGraph Images for Card Previews -description: Extract GitHub OpenGraph Images for Card Previews -dateFormatted: Dec 19th, 2023 ---- - -Previously, when sharing GitHub on my blog, I always used [GitHub Repository Card](https://gh-card.dev/) for sharing, but it doesn't have good support for Chinese and doesn't support line breaks. - -[![ccbikai/cloudflare-worker-image - GitHub](https://gh-card.dev/repos/ccbikai/cloudflare-worker-image.svg?fullname=)](https://github.com/ccbikai/cloudflare-worker-image) - -Originally, I planned to create my own using [@vercel/og](https://vercel.com/docs/functions/edge-functions/og-image-generation), but I accidentally discovered that GitHub provides comprehensive and beautiful Open Graph images on Twitter. So, I wrote a script to extract and use them for blog previews. - -## Demo - -![nasa/fprime - GitHub](https://github.html.zone/nasa/fprime) - -![A framework for building Open Graph images](https://static.miantiao.me/share/9ZxTs8/RZHfnD.png) - -In addition to repositories, GitHub's Open Graph also supports previews for Issue, Pull Request, Discussion, and Commit modules. - -## Usage - -**Modify `.com` to `.html.zone` on any GitHub page**. - -For example, [https://github.com/vercel/next.js](https://github.com/vercel/next.js) => [https://github.html.zone/vercel/next.js](https://github.html.zone/vercel/next.js). - -### Previews - -#### Repo - -![Repo](https://github.html.zone/vercel/next.js) - -#### Issue - -![Issue](https://github.html.zone/vuejs/core/issues/9862) - -#### Pull Request - -![Pull Request](https://github.html.zone/lobehub/lobe-chat/pull/529) - -#### Discussion - -![Discussion](https://github.html.zone/lobehub/lobe-chat/discussions/551) - -#### Commit - -![Commit](https://github.html.zone/vercel/next.js/commit/a65fb162989fd00ca21534947538b8dbb6bf7f86) - -## Source Code - -The code has been shared on GitHub for those interested to explore. - -[![ccbikai/github-og-image - GitHub](https://github.html.zone/ccbikai/github-og-image)](https://github.com/ccbikai/github-og-image) diff --git a/src/content/post/google-safe-browsing-alternative.md b/src/content/post/google-safe-browsing-alternative.md deleted file mode 100644 index 95b4543..0000000 --- a/src/content/post/google-safe-browsing-alternative.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: How to Replace Google Safe Browsing with Cloudflare Zero Trust -description: How to Replace Google Safe Browsing with Cloudflare Zero Trust -dateFormatted: Jul 14th, 2024 ---- - -So, get this, right? I built the first version of [L(O\*62).ONG](https://loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.ong/) using server-side redirects, but Google slapped me with a security warning the very next day. Talk about a buzzkill! I had to scramble and switch to local redirects with a warning message before sending folks on their way. Then came the fun part – begging Google for forgiveness. - -Now, the smart money would've been on using Google Safe Browsing for redirects. But here's the catch: Safe Browsing's got a daily limit – 10,000 calls, and that's it. Plus, no custom lists. And since I'm all about keeping things simple and sticking with Cloudflare, Safe Browsing was a no-go. - -Fast forward to a while back, I was chewing the fat with someone online, and bam! It hit me like a bolt of lightning. Why not use a secure DNS server with built-in filters for adult content and all that shady stuff to check if a domain's on the up-and-up? Figured I'd give [Family 1.1.1.1](https://blog.cloudflare.com/zh-cn/introducing-1-1-1-1-for-families-zh-cn/) a shot, and guess what? It actually worked! Problem was, no custom lists there either. Then I remembered messing around with Cloudflare Zero Trust Gateway back in my [HomeLab](https://www.awesome-homelab.com/) days. Turns out, that was the golden ticket – a solution so good, it's almost criminal. - -**Here's the deal: Cloudflare Zero Trust's Gateway comes packing a built-in DNS (DoH) server and lets you set up firewall rules like a boss. You can block stuff based on how risky a domain is, what kind of content it has, and even use your own custom naughty-and-nice lists. And get this – it pulls data from Cloudflare's own stash, over 30 open intelligence sources, fancy machine learning models, and even feedback from the community. Talk about covering all the bases! Want the nitty-gritty? Hit up the [official documentation](https://developers.cloudflare.com/cloudflare-one/policies/gateway/domain-categories/#docs-content).** - -So, I went ahead and blocked all the high-risk categories – adult stuff, gambling sites, government domains, anything NSFW, newly registered domains, you name it. Plus, I've got my own little blacklists and whitelists that I keep nice and tidy. - -![Risk List](https://static.miantiao.me/share/2024/ROJmki/CleanShot%202024-07-07%20at%2022.22.25.png) - -Once I was done tweaking the settings, I got myself a shiny new DoH address: - -![DoH](https://static.miantiao.me/share/2024/iY5dK8/CleanShot%202024-07-07%20at%2022.26.23.png) - -To hook it up to my project, I used this handy-dandy code: - -``` -async function isSafeUrl( - url, - DoH = "https://family.cloudflare-dns.com/dns-query" -) { - let safe = false; - try { - const { hostname } = new URL(url); - const res = await fetch(`${DoH}?type=A&name=${hostname}`, { - headers: { - accept: "application/dns-json", - }, - cf: { - cacheEverything: true, - cacheTtlByStatus: { "200-299": 86400 }, - }, - }); - const dnsResult = await res.json(); - if (dnsResult && Array.isArray(dnsResult.Answer)) { - const isBlock = dnsResult.Answer.some( - answer => answer.data === "0.0.0.0" - ); - safe = !isBlock; - } - } catch (e) { - console.warn("isSafeUrl fail: ", url, e); - } - return safe; -} - -``` - -And here's the kicker: Cloudflare Zero Trust's management panel has this sweet visualization interface that lets you see what's getting blocked and what's not. You can see for yourself – it's got the kibosh on some adult sites and those brand-spanking-new domains. - -![Visualization Interface](https://static.miantiao.me/share/2024/5hOp5X/CleanShot%202024-07-07%20at%2022.30.36.png) - -Oh, and if a domain ends up on the wrong side of the tracks, you can always check the log to see what went down. - -![Log](https://static.miantiao.me/share/2024/EmRMB3/52WCkd.png) diff --git a/src/content/post/looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong.md b/src/content/post/looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong.md deleted file mode 100644 index 8f993c2..0000000 --- a/src/content/post/looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: L(O*62).ONG - Make your URL longer -description: loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.ong is the longest domain name -dateFormatted: Jun 1th, 2024 ---- - -[![GitHub](https://github.html.zone/ccbikai/loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.ong)](https://github.com/ccbikai/loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.ong) - -This little toy was finished last week. Just a few lines of code. - -Encountered many issues during deployment, mainly related to HTTPS certificates. - -The longest segment of the domain name is 63 characters. The commonName of the HTTPS certificate can be up to 64 characters. - -This caused Cloudflare, Vercel, and Netlify to be unable to use Let's Encrypt to sign HTTPS certificates (because they use the domain name in commonName), but Zeabur can use Let's Encrypt to sign HTTPS certificates. - -Finally, switching the Cloudflare certificate to Google Trust Services LLC successfully signed the certificate. - -You can view the relevant certificates at [https://crt.sh/?q=loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.ong](https://crt.sh/?q=loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.ong). diff --git a/src/content/post/sink.md b/src/content/post/sink.md deleted file mode 100644 index 3bd8ebf..0000000 --- a/src/content/post/sink.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Sink - A short link system based on Cloudflare with visit statistics -description: A short link system based on Cloudflare with visit statistics -dateFormatted: Jun 4th, 2024 ---- - -I previously shared some websites on [Twitter](https://x.com/0xKaiBi) using short links to make it easier to see if people are interested. Among these link shortening systems, Dub provides the best user experience, but it has a fatal flaw: once the monthly clicks exceed 1000, you can no longer view the statistics. - -While surfing the internet at home during the Qingming Festival, I discovered that [Cloudflare Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/) supports data writing and API data querying. So, I created an MVP version myself, capable of handling statistics for up to 3,000,000 visits per month. Cloudflare's backend likely uses Clickhouse, so performance shouldn't be a significant issue. - -During the Labor Day holiday, I improved the frontend UI at home and used it for about half a month, finding it satisfactory. I have open-sourced it for everyone to use. - -## Features - -- Link shortening -- Visit statistics -- Serverless deployment -- Custom Slug -- 🪄 AI-generated Slug -- Link expiration - -## Demo - -[Sink.Cool](https://sink.cool/dashboard) - -Site Token: `SinkCool` - -### Site-wide Analysis - -![Site-wide Analysis](https://static.miantiao.me/share/CBuVes/sink.cool_dashboard.png) - -
- Link Management - Link Management -
- - -
- Individual Link Analysis - Individual Link Analysis -
- -## Open Source - -[![ccbikai/sink - GitHub](https://github.html.zone/ccbikai/sink)](https://github.com/ccbikai/sink) - -## Roadmap (WIP) - -- Browser extension -- Raycast extension -- Apple Shortcuts -- Enhanced link management (based on Cloudflare D1) -- Enhanced analysis (support filtering) -- Panel performance optimization (support infinite loading) -- Support for other platforms (maybe) - ---- - -Finally, feel free to follow me on [Twitter](https://x.com/0xKaiBi) for updates on development progress and to share some web development news. diff --git a/src/content/post/umami-kill-adblock.md b/src/content/post/umami-kill-adblock.md deleted file mode 100644 index 0759fae..0000000 --- a/src/content/post/umami-kill-adblock.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Resolving Umami Blocked by AdBlock Issue -description: Resolving Umami Blocked by AdBlock Issue -dateFormatted: Jan 6th, 2024 ---- - -I recently redesigned my [personal homepage](https://mt.ci/) and used Umami for website analytics. However, there is an ongoing issue: users who have AdBlock installed are causing the analytics to fail. - -For more information on how AdBlock works, you can refer to [Resolving Vercel Analytics Blocked by AdBlock Issue](11). The rule that blocks Umami is `||umami.is^$3p`, which blocks the script and data reporting URLs. To overcome this, we can use [Cloudflare Workers](https://workers.cloudflare.com/) to proxy Umami. - -![||umami.is^$3p](https://static.miantiao.me/share/2024/CNrM78/ha30pV.png) - -## Solution - -Create a Cloudflare Worker and paste the following JavaScript code. If you are using the official Umami service, you don't need to modify the code (remember to change UMAMI\_HOST to your service URL). If you are using a self-hosted service, you can define the script and data reporting URLs using the `TRACKER_SCRIPT_NAME` and `COLLECT_API_ENDPOINT` environment variables, without the need for proxying. - -```js -const UMAMI_HOST = 'https://eu.umami.is' - -export default { - async fetch(request, env, ctx) { - const { pathname, search } = new URL(request.url) - if (pathname.endsWith('.js')) { - let response = await caches.default.match(request) - if (!response) { - response = await fetch(`${UMAMI_HOST}/script.js`, request) - ctx.waitUntil(caches.default.put(request, response.clone())) - } - return response - } - const req = new Request(request) - req.headers.delete("cookie") - req.headers.append('x-client-ip', req.headers.get('cf-connecting-ip')) - return fetch(`${UMAMI_HOST}${pathname}${search}`, req) - }, -}; - -``` - -Once you have created the Worker, configure the domain and test if the script URL can be accessed correctly. In my case, it is [https://ums.miantiao.me/mt-demo.js](https://ums.miantiao.me/mt-demo.js). You can replace "mt-demo" with any disguised URL, as the script has already been adapted. - -Next, inject the script into your website project. You can refer to the official documentation at [https://umami.is/docs/tracker-configuration](https://umami.is/docs/tracker-configuration) or use the following code as a reference: - -```html - - -``` - -In the above code, `src` refers to the script URL, `data-host-url` refers to the data reporting URL, and `data-website-id` refers to the website ID. Make sure to provide the correct website ID to ensure data reporting. - -You can verify the implementation on [Noodle Lab](https://mt.ci/) or this website. diff --git a/src/content/post/vercel-edge-image.md b/src/content/post/vercel-edge-image.md deleted file mode 100644 index fdc4217..0000000 --- a/src/content/post/vercel-edge-image.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Using Vercel Edge to Process Images -description: Using Vercel Edge to Process Images -dateFormatted: Dec 17th, 2023 ---- - -Previously, I shared an article on [using Cloudflare Worker to process images](https://dev.to/ccbikai/shi-yong-cloudflare-worker-chu-li-tu-pian-38dl-temp-slug-7437591). However, due to the limitations of the free version of Worker, which only allows for 10ms of CPU usage, there were frequent resource overages and high failure rates. Today, I had some free time, so I decided to try using Vercel Edge instead and share my findings with those who are interested. - -The official version of Vercel also supports image processing, but it has a limit of 1000 original images per month and only supports scaling. By using Vercel Edge to process images, you can have additional features such as scaling, cropping, watermarking, and filters. However, please note that the free version of Vercel only allows for 100GB of monthly traffic, so it is recommended to use it in conjunction with a CDN for actual usage. - -Supported features: - -1. Support for processing PNG, JPG, BMP, ICO, and TIFF format images -2. Output images in JPG, PNG, and WEBP formats, with WEBP being the default -3. Support for pipelining, allowing for multiple operations to be performed -4. Support for whitelisting image URLs to prevent abuse -5. Graceful degradation in case of processing failure, returning the original image (exceptions are not cached) - -## Demo - -### Format Conversion - -#### WEBP - -![webp](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&format=webp) - -#### JPG - -![jpg](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&format=jpg) - -#### PNG - -![png](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&format=png) - -### Scaling - -![resize](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=resize!830,400,2) - -### Rotation - -![rotate](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=rotate!90) - -### Cropping - -![rotate](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=crop!0,0,1000,1000) - -### Filters - -![filter](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=filter%21obsidian) - -### Image Watermark - -![watermark](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=watermark!https%3A%2F%2Fstatic.miantiao.me%2Fshare%2F6qIq4w%2FFhSUzU.png,20,20) - -### Text Watermark - -![draw_text](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=draw_text!miantiao.me,20,20) - -### Pipelining - -#### Scaling + Rotation + Text Watermark - -![resize & rotate & draw_text](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=resize!830,400,2%7Crotate!180%7Cdraw_text!miantiao.me,10,10) - -#### Scaling + Image Watermark - -![resize & watermark](https://edge-image.miantiao.me/?url=https%3A%2F%2Fstatic.miantiao.me%2Fshare%2FMTyerw%2Fbanner-2048.jpeg&action=resize!830,400,2%7Cwatermark!https%3A%2F%2Fstatic.miantiao.me%2Fshare%2F6qIq4w%2FFhSUzU.png,10,10) - -In theory, it supports various operations available in Photon. If you are interested, you can check the image URLs and modify the parameters according to the [Photon documentation](https://docs.rs/photon-rs/latest/photon_rs/) to try it out yourself. If you encounter any issues, please leave a comment and provide feedback. - -## Sharing - -I have open-sourced this solution on my GitHub repository, and you can deploy it by following the documentation. - -[![ccbikai/vercel-edge-image - GitHub](https://github.html.zone/ccbikai/vercel-edge-image)](https://github.com/ccbikai/vercel-edge-image) - -* * * - -[![Buy Me A Coffee](https://static.miantiao.me/share/0WmsVP/CcmGr8.png)](https://www.buymeacoffee.com/ccbikai) \ No newline at end of file diff --git a/src/content/post/vercel-kill-adblock.md b/src/content/post/vercel-kill-adblock.md deleted file mode 100644 index 08fe1ae..0000000 --- a/src/content/post/vercel-kill-adblock.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -layout: ../../layouts/post.astro -title: Solving Vercel Analytics Blocked by AdBlock Issue -description: Solving Vercel Analytics Blocked by AdBlock Issue -dateFormatted: Jun 6th, 2024 ---- - -[DNS.Surf](https://dns.surf/) runs 100% on Vercel, so Vercel Analytics is used for access statistics. However, many users who have AdBlock installed experience issues with access statistics not being recorded. Today, we will solve the problem of AdBlock blocking access statistics, while still relying on Vercel 100%. - -The core principle of AdBlock is to block certain network requests and page elements using rules. Vercel Analytics is blocked by the rule `/_vercel/insights/script.js`, and it may also block `/_vercel/insights/event`. To solve this problem, we just need to make these two URLs less recognizable. - -![/_vercel/insights/script.js](https://static.miantiao.me/share/2024/JbSVLo/5aOZdV.png) - -## Solution - -Vercel comes with a Rewrite feature, so we just need to rewrite the disguised path `/mt-demo` to `/_vercel/insights`. The disguised path can be any unique path that does not conflict with existing paths. If it gets blocked, just use a different one. The vercel.json configuration is as follows: - -```js -{ - "rewrites": [ - { - "source": "/mt-demo/:match*", - "destination": "https://dns.surf/_vercel/insights/:match*" - } - ] -} -``` - -Note that the destination should be the complete URL, otherwise it will not work. - -In the official tutorial, different frameworks use [@vercel/analytics](https://vercel.com/docs/analytics/package) to inject the analytics script into the page, but it does not support custom scripts and data reporting URLs. Therefore, we need to use the HTML method to inject the script. - -```html - - -``` - -`src` is the script URL, and `data-endpoint` is the data reporting URL. Although it is not mentioned in the official documentation, the script does support it. Remember to replace `mt-demo` with your disguised path. - -If you are using a different framework, you can look for the method to inject scripts in that framework to adapt it to your own usage. - -You can verify the effect using [DNS.Surf](https://dns.surf/). diff --git a/src/data/experiences.json b/src/data/experiences.json new file mode 100644 index 0000000..ba05410 --- /dev/null +++ b/src/data/experiences.json @@ -0,0 +1,37 @@ +[ + { + "dates": "2023 · Présent", + "role": "Consultant Développeur Senior", + "company": "Urssaf Caisse nationale", + "description": "Mission freelance - Architecture et développement d'applications métier.", + "logo": "" + }, + { + "dates": "2018 · Présent", + "role": "Organisateur", + "company": "Software Crafters Albi", + "description": "Animation de la communauté locale de développeurs passionnés par le craft.", + "logo": "" + }, + { + "dates": "2015 · 2016", + "role": "Fondateur", + "company": "while42", + "description": "Réseau mondial de développeurs expatriés - communauté d'entraide et de partage.", + "logo": "" + }, + { + "dates": "2003 · 2006", + "role": "Créateur", + "company": "Projets personnels", + "description": "N.Gine (CMS propriétaire), ICU (plateforme de partage photo), Débats.co (débat politique collaboratif).", + "logo": "" + }, + { + "dates": "2001 · 2003", + "role": "Étudiant", + "company": "UVSQ - Université de Versailles", + "description": "Meilleur projet de programmation de l'année 2003.", + "logo": "" + } +] \ No newline at end of file diff --git a/src/data/favorites.json b/src/data/favorites.json new file mode 100644 index 0000000..6f42e10 --- /dev/null +++ b/src/data/favorites.json @@ -0,0 +1,8 @@ +[ + "portraits/Eroll.jpg", + "places/Court-of-Audit-Paris.jpg", + "places/Petra.jpg", + "places/Au-pied-de-la-Tour-Eiffel.jpg", + "places/In-Ceutas-fog.jpg", + "places/Les-Houches.jpg" +] diff --git a/src/data/menu.json b/src/data/menu.json new file mode 100644 index 0000000..d9dd7b8 --- /dev/null +++ b/src/data/menu.json @@ -0,0 +1,10 @@ +[ + { + "name": "Photo", + "url": "/photo" + }, + { + "name": "À propos", + "url": "/a-propos" + } +] diff --git a/src/data/menu.ts b/src/data/menu.ts new file mode 100644 index 0000000..3056271 --- /dev/null +++ b/src/data/menu.ts @@ -0,0 +1,138 @@ +export interface MenuItem { + name: Record; + url: string; + icon?: string; + children?: MenuItem[]; +} + +export const mainMenu: MenuItem[] = [ + { + name: { + fr: "Accueil", + en: "Home", + ar: "الرئيسية" + }, + url: "/" + }, + { + name: { + fr: "Pro", + en: "Pro", + ar: "مهني" + }, + url: "/pro", + children: [ + { + name: { + fr: "Blog Pro", + en: "Pro Blog", + ar: "مدونة مهنية" + }, + url: "/blog/pro" + }, + { + name: { + fr: "Projets", + en: "Projects", + ar: "مشاريع" + }, + url: "/projects" + }, + { + name: { + fr: "Talks", + en: "Talks", + ar: "محاضرات" + }, + url: "/talks" + } + ] + }, + { + name: { + fr: "Comédie", + en: "Comedy", + ar: "كوميديا" + }, + url: "/comedy", + children: [ + { + name: { + fr: "Blog Comédie", + en: "Comedy Blog", + ar: "مدونة الكوميديا" + }, + url: "/blog/comedy" + }, + { + name: { + fr: "Spectacles", + en: "Shows", + ar: "عروض" + }, + url: "/shows" + } + ] + }, + { + name: { + fr: "Photo", + en: "Photo", + ar: "تصوير" + }, + url: "/photo", + children: [ + { + name: { + fr: "Galerie", + en: "Gallery", + ar: "معرض" + }, + url: "/gallery" + }, + { + name: { + fr: "Blog Photo", + en: "Photo Blog", + ar: "مدونة التصوير" + }, + url: "/blog/photo" + } + ] + }, + { + name: { + fr: "Blog", + en: "Blog", + ar: "مدونة" + }, + url: "/blog" + }, + { + name: { + fr: "À propos", + en: "About", + ar: "حول" + }, + url: "/about" + } +]; + +export const slashPages: MenuItem[] = [ + { + name: { + fr: "Maintenant", + en: "Now", + ar: "الآن" + }, + url: "/now" + }, + { + name: { + fr: "Utilise", + en: "Uses", + ar: "يستخدم" + }, + url: "/uses" + } +]; \ No newline at end of file diff --git a/src/collections/projects.json b/src/data/projects.json similarity index 70% rename from src/collections/projects.json rename to src/data/projects.json index 15a0b4d..cd8861e 100644 --- a/src/collections/projects.json +++ b/src/data/projects.json @@ -10,11 +10,5 @@ "description": "Querying DNS Resolution Results in Different Regions Worldwide.", "image": "/assets/images/projects/dns.surf.png", "url": "https://dns.surf" - }, - { - "name": "HTML.ZONE", - "description": "Web Toolbox.", - "image": "/assets/images/projects/html.zone.png", - "url": "https://html.zone" } ] diff --git a/src/layouts/PhotoLayout.astro b/src/layouts/PhotoLayout.astro new file mode 100644 index 0000000..c535cca --- /dev/null +++ b/src/layouts/PhotoLayout.astro @@ -0,0 +1,44 @@ +--- +const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false, hideFooter = false } = Astro.props; +--- + + + + + + + {title} + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/layouts/main.astro b/src/layouts/main.astro index 88f8ee8..7c9ee3b 100644 --- a/src/layouts/main.astro +++ b/src/layouts/main.astro @@ -29,7 +29,7 @@ const { title } = Astro.props; border-radius: 20px; } - + diff --git a/src/pages/a-propos.astro b/src/pages/a-propos.astro new file mode 100644 index 0000000..da4b589 --- /dev/null +++ b/src/pages/a-propos.astro @@ -0,0 +1,54 @@ +--- +import PageHeading from "../components/page-heading.astro"; +import Layout from "../layouts/main.astro"; +import Link from "../components/Link.astro"; +--- + + +
+ + + Jalil Arfaoui + +

Qui suis-je ?

+
+

+ Je suis Jalil Arfaoui, développeur freelance basé à Albi avec plus de 20 ans d'expérience. + Mon credo : "Construire des logiciels bien pensés qui répondent à de vrais besoins". +

+

+ Passionné par le Software Craftsmanship, je pratique le TDD, le Clean Code et le Domain-Driven Design au quotidien. + J'accompagne les équipes en tant que développeur senior, tech lead ou coach technique selon les besoins. +

+

+ Mon stack de prédilection : TypeScript/JavaScript, mais aussi PHP et Elixir. + Ce qui m'anime : construire des architectures propres et maintenables, et transmettre ces pratiques. +

+

+ Organisateur des Software Crafters Albi depuis 2018, j'aime rassembler et partager avec d'autres passionnés du code. +

+
+ +

Au-delà du code

+
+

+ 🎭 Théâtre — Amateur passionné, je monte sur scène régulièrement. + Le théâtre m'apporte une autre façon de raconter des histoires et de me connecter aux autres. +

+

+ 📸 Photographie — Capturer l'instant, jouer avec la lumière. + Une passion qui me suit depuis des années et que je partage sur ce site. +

+
+ +

Me contacter

+

+ Envie d'échanger sur un projet, une mission ou juste discuter craft ? + + · Écrivez-moi +

+
+
\ No newline at end of file diff --git a/src/pages/about.astro b/src/pages/about.astro deleted file mode 100644 index 54c75b0..0000000 --- a/src/pages/about.astro +++ /dev/null @@ -1,72 +0,0 @@ ---- -import experiences from "../collections/experiences.json"; -import AboutExperience from "../components/about-experience.astro"; -import PageHeading from "../components/page-heading.astro"; -import Layout from "../layouts/main.astro"; ---- - - -
- - - - -

Short Bio

-

- Front-end cutter 🧑🏻‍💻, back-end amateur 🤷🏻‍♂️, operations digging holes person 🤦🏻‍♂️. -

- -

- Experience -

-
- { - experiences.map((experience, loop) => { - return ( - <> - {loop == 0 || loop == 1 ? ( -
- -
- ) : ( - - )} - - ) - }) - } -
- -

Let's Connect

-

- If you want to stay up to date with my work be sure to follow me on twitter, or you can send me an email and I'll be sure to get back to you. -

-
-
diff --git a/src/pages/ar/index.astro b/src/pages/ar/index.astro new file mode 100644 index 0000000..b7dd929 --- /dev/null +++ b/src/pages/ar/index.astro @@ -0,0 +1,181 @@ +--- +import Layout from "../../layouts/main.astro"; +--- + + + +
+
+
+

+ جليل عرفاوي +

+

+ مطوّر • عاشق للمسرح • هاوي للتصوير +

+

+ مرحبًا بكم في عالمي الإبداعي حيث يلتقي الكود بالفن +

+ +
+ + اكتشف أعمالي + 🚧 + + + تعرّف عليّ أكثر + +
+
+ +
+
+
+
+
+ جليل عرفاوي +
+
+
+
+
+
+ + +
+
+ + +
+
💻
+
+

+ المطوّر +

+

+ شغوف بحرفة البرمجة، TDD والهندسة النظيفة. خبير في TypeScript و Node.js و DevOps. +

+
+ + ← المسار المهني 🚧 + + + ← مشاريعي 🚧 + + + ← المدوّنة التقنية 🚧 + +
+
+
+ + +
+
🎭
+
+

+ المسرح +

+

+ عاشق للمسرح ومبدع للمحتوى الفكاهي. من خشبة المسرح إلى وسائل التواصل الاجتماعي، استكشاف فن الإضحاك. +

+
+ + ← المسار الفني 🚧 + + + ← عروضي 🚧 + + + ← مدوّنة المسرح 🚧 + +
+
+
+ + +
+
📸
+
+

+ التصوير +

+

+ هاوي تصوير فوتوغرافي، ألتقط اللحظات وأروي القصص من خلال العدسة. +

+
+ + ← معرض الصور + + + ← الألبومات 🚧 + + + ← مدوّنة التصوير 🚧 + +
+
+
+
+
+ + +
+
+

+ آخر المقالات +

+

+ اكتشف أفكاري حول البرمجة والمسرح والتصوير +

+
+ +
+ + اكتشف جميع المقالات + 🚧 + +
+
+ + +
+
+

+ لنبقَ على تواصل +

+

+ سواء لمشروع مهني، أو تعاون إبداعي، أو مجرد حديث، لا تتردد في التواصل! +

+ + +
+
+
\ No newline at end of file diff --git a/src/pages/ar/نبذة-عني.astro b/src/pages/ar/نبذة-عني.astro new file mode 100644 index 0000000..96bba2d --- /dev/null +++ b/src/pages/ar/نبذة-عني.astro @@ -0,0 +1,54 @@ +--- +import PageHeading from "../../components/page-heading.astro"; +import Layout from "../../layouts/main.astro"; +import Link from "../../components/Link.astro"; +--- + + +
+ + + جليل عرفاوي + +

من أنا؟

+
+

+ أنا جليل عرفاوي، مطوّر برمجيات مستقل مقيم في ألبي بفرنسا، بخبرة تتجاوز عشرين عامًا. + شعاري: "بناء برمجيات مدروسة تلبي احتياجات حقيقية". +

+

+ شغوف بـحرفة البرمجة (Software Craftsmanship)، أمارس يوميًا منهجيات TDD والكود النظيف وتصميم المجالات. + أرافق الفرق كمطوّر أول، أو قائد تقني، أو مدرب تقني حسب الحاجة. +

+

+ تقنياتي المفضلة: TypeScript/JavaScript، بالإضافة إلى PHP و Elixir. + ما يحرّكني: بناء هياكل برمجية نظيفة وقابلة للصيانة، ونقل هذه الممارسات للآخرين. +

+

+ منظّم مجتمع Software Crafters Albi منذ 2018، أحب الجمع والمشاركة مع المتحمسين للبرمجة. +

+
+ +

ما وراء الكود

+
+

+ 🎭 المسرح — هاوٍ شغوف، أصعد على خشبة المسرح بانتظام. + المسرح يمنحني طريقة أخرى لسرد القصص والتواصل مع الناس. +

+

+ 📸 التصوير الفوتوغرافي — التقاط اللحظة، اللعب بالضوء. + شغف يرافقني منذ سنوات، أشاركه على هذا الموقع. +

+
+ +

تواصل معي

+

+ تريد مناقشة مشروع، مهمة، أو مجرد الحديث عن البرمجة؟ + + · راسلني +

+
+
\ No newline at end of file diff --git a/src/pages/en/about.astro b/src/pages/en/about.astro new file mode 100644 index 0000000..4353451 --- /dev/null +++ b/src/pages/en/about.astro @@ -0,0 +1,54 @@ +--- +import PageHeading from "../../components/page-heading.astro"; +import Layout from "../../layouts/main.astro"; +import Link from "../../components/Link.astro"; +--- + + +
+ + + Jalil Arfaoui + +

Who am I?

+
+

+ I'm Jalil Arfaoui, a freelance developer based in Albi, France with over 20 years of experience. + My motto: "Building well-thought software that meets real needs". +

+

+ Passionate about Software Craftsmanship, I practice TDD, Clean Code and Domain-Driven Design on a daily basis. + I support teams as a senior developer, tech lead or technical coach depending on their needs. +

+

+ My preferred stack: TypeScript/JavaScript, but also PHP and Elixir. + What drives me: building clean, maintainable architectures and sharing these practices with others. +

+

+ Organizer of Software Crafters Albi since 2018, I love bringing together and sharing with fellow code enthusiasts. +

+
+ +

Beyond code

+
+

+ 🎭 Theater — Passionate amateur actor, I regularly perform on stage. + Theater gives me another way to tell stories and connect with people. +

+

+ 📸 Photography — Capturing the moment, playing with light. + A passion that has followed me for years, which I share on this site. +

+
+ +

Get in touch

+

+ Want to discuss a project, a mission, or just talk craft? + + · Email me +

+
+
\ No newline at end of file diff --git a/src/pages/en/index.astro b/src/pages/en/index.astro new file mode 100644 index 0000000..63be7f4 --- /dev/null +++ b/src/pages/en/index.astro @@ -0,0 +1,181 @@ +--- +import Layout from "../../layouts/main.astro"; +--- + + + +
+
+
+

+ Jalil Arfaoui +

+

+ Developer • Theater enthusiast • Photography lover +

+

+ Welcome to my creative universe where code meets art +

+ +
+ + View my work + 🚧 + + + Learn more + +
+
+ +
+
+
+
+
+ Jalil Arfaoui +
+
+
+
+
+
+ + +
+
+ + +
+
💻
+
+

+ Developer +

+

+ Passionate about Software Craftsmanship, TDD and clean architecture. TypeScript, Node.js and DevOps expert. +

+
+ + → Professional journey 🚧 + + + → My projects 🚧 + + + → Technical blog 🚧 + +
+
+
+ + +
+
🎭
+
+

+ Theater +

+

+ Theater enthusiast and creator of humorous content. Exploring the art of making people laugh, from stage to social media. +

+
+ + → Artistic journey 🚧 + + + → My shows 🚧 + + + → Theater blog 🚧 + +
+
+
+ + +
+
📸
+
+

+ Photography +

+

+ Photography hobbyist capturing moments and telling stories through the lens. +

+
+ + → Photo portfolio + + + → Galleries 🚧 + + + → Photo blog 🚧 + +
+
+
+
+
+ + +
+
+

+ Latest articles +

+

+ Discover my thoughts on development, theater and photography +

+
+ +
+ + Discover all my articles + 🚧 + +
+
+ + +
+
+

+ Let's stay in touch +

+

+ Whether for a professional project, creative collaboration or just to chat, feel free to reach out! +

+ + +
+
+
\ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index b901554..35a86c0 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,76 +1,169 @@ --- -import Button from "../components/button.astro"; -import Projects from "../components/home/projects.astro"; -import Separator from "../components/home/separator.astro"; -import Writings from "../components/home/writings.astro"; import Layout from "../layouts/main.astro"; --- - -
+ + +
-

- Hello, I'm Kai. +

+ Jalil Arfaoui

-

- I'm a front-end programmer living in Nanjing. I focus on Web development. +

+ Développeur • Comédien • Photographe +

+

+ Bienvenue dans mon univers créatif où le code rencontre l'art

-

- I can help you out with: -

-
    -
  • Vue.js Development
  • -
  • React.js Development
  • -
  • Node.js Development
  • -
  • Website Design
  • -
  • and more...
  • -
-
-