[{"data":1,"prerenderedAt":283},["ShallowReactive",2],{"project-\u002Fprojects\u002Fportfolio-photo-fr":3,"related-\u002Fprojects\u002Fportfolio-photo-fr":262,"legal-link-fr":281},{"id":4,"title":5,"body":6,"description":240,"extension":241,"git":242,"git_soon":245,"meta":246,"navigation":247,"order":248,"path":249,"seo":250,"stack":251,"stem":255,"type":256,"updated":257,"web":258,"__hash__":261},"projects_fr\u002Fprojects\u002Fportfolio-photo.md","Portfolio Photo",{"type":7,"value":8,"toc":224},"minimark",[9,17,22,25,29,82,86,148,152,158,161,165,170,176,178,182,188,190,194,200,202,206,212,214,218],[10,11,12,16],"p",{},[13,14,15],"strong",{},"Remplacer un abonnement Adobe Lightroom par mon propre outil"," : un portfolio photo et son back-office d'administration, du stockage objet à la galerie publique, entièrement maison.",[18,19,21],"h2",{"id":20},"quel-est-le-contexte-du-portfolio-photo","Quel est le contexte du Portfolio Photo ?",[10,23,24],{},"J'utilisais Adobe Lightroom pour publier mes photos en ligne, mais l'abonnement Creative Cloud représente un coût mensuel disproportionné pour un usage essentiellement statique. J'ai donc développé mon propre portfolio photographique from scratch, avec un back-office d'administration intégré pour gérer albums et photos directement depuis le navigateur. Le projet couvre toute la chaîne : stockage objet, redimensionnement à la volée, galerie publique et interface d'administration.",[18,26,28],{"id":27},"quelle-stack-technique","Quelle stack technique ?",[30,31,32,44,50,56,66,72],"ul",{},[33,34,35,38,39,43],"li",{},[13,36,37],{},"Astro 4 (hybrid)"," — mode ",[40,41,42],"code",{},"output: 'hybrid'"," : les pages publiques sont statiques (pré-rendues au build), les routes d'API et l'admin tournent en SSR. Évite un framework full-SPA pour du contenu majoritairement statique.",[33,45,46,49],{},[13,47,48],{},"Vue 3"," — utilisé uniquement pour les composants interactifs (galerie, lightbox, admin). Astro charge Vue à la demande via les islands, le reste est HTML pur.",[33,51,52,55],{},[13,53,54],{},"Cloudflare R2"," — stockage objet S3-compatible. Pas d'egress fee sur les lectures publiques, interface compatible AWS SDK. Les métadonnées des albums sont stockées en JSON directement dans R2 (pas de base de données).",[33,57,58,61,62,65],{},[13,59,60],{},"Cloudflare Image Resizing"," — redimensionnement à la volée via URL (",[40,63,64],{},"\u002Fcdn-cgi\u002Fimage\u002Fwidth=1200,quality=78,format=webp\u002F...","). Aucune pré-génération de vignettes nécessaire, cache CDN intégré.",[33,67,68,71],{},[13,69,70],{},"Netlify"," — hébergement avec adapter Astro. Un webhook de build permet de re-déployer le site statique depuis l'admin (bouton \"Publier\").",[33,73,74,77,78,81],{},[13,75,76],{},"justified-layout"," — bibliothèque Flickr open-source pour le calcul des positions de la grille justifiée. Recalcul via ",[40,79,80],{},"ResizeObserver"," à chaque changement de largeur du conteneur.",[18,83,85],{"id":84},"points-techniques-notables","Points techniques notables",[30,87,88,98,112,118,128,142],{},[33,89,90,93,94,97],{},[13,91,92],{},"Upload direct navigateur → R2 via presigned URL"," : l'API ",[40,95,96],{},"\u002Fapi\u002Fpresign"," génère une URL signée S3 (TTL 3600s), le navigateur PUT le fichier directement sur R2 sans transiter par le serveur Astro. Limite les coûts bande passante et les timeouts sur les gros fichiers.",[33,99,100,103,104,107,108,111],{},[13,101,102],{},"Pas de base de données"," : les métadonnées des albums et photos sont stockées sous forme de JSON dans R2 (",[40,105,106],{},"albums\u002Findex.json",", ",[40,109,110],{},"albums\u002F{slug}.json","). Suffisant pour une collection personnelle, élimine un service à maintenir.",[33,113,114,117],{},[13,115,116],{},"Lightbox custom"," (sans dépendance externe) : progressive loading (miniature floutée → full-res), zoom molette\u002Fpinch\u002Fdouble-tap (max 5×), pan avec clamping, navigation swipe, support clavier complet, affichage des données EXIF (appareil, objectif, ouverture, vitesse, ISO, lieu).",[33,119,120,123,124,127],{},[13,121,122],{},"Métadonnées EXIF préservées de bout en bout"," : le script CLI extrait via ",[40,125,126],{},"exifr"," les données EXIF (appareil, objectif, GPS, date de prise de vue) et les stocke structurées dans les JSON d'album. Elles sont affichées dans la lightbox publique.",[33,129,130,133,134,137,138,141],{},[13,131,132],{},"Script d'import batch"," (",[40,135,136],{},"npx tsx scripts\u002Fimport.ts --album tokyo-2025 ~\u002FPhotos\u002F*.jpg",") : upload concurrent (3 fichiers en parallèle), extraction EXIF automatique, cache ",[40,139,140],{},"immutable"," 1 an sur les objets R2, mise à jour atomique de l'index.",[33,143,144,147],{},[13,145,146],{},"Rebuild statique à la demande"," : le bouton \"Publier le site\" en admin déclenche un POST sur un build hook Netlify — le site public est regénéré statiquement avec les nouvelles données, sans exposer d'endpoint de contenu dynamique en production.",[18,149,151],{"id":150},"ce-que-jai-appris","Ce que j'ai appris",[10,153,154,155,157],{},"Le challenge principal était l'hébergement et la configuration Cloudflare : comprendre le modèle hybride SSR\u002FSSG d'Astro sur Netlify, mettre en place le pipeline de delivery via Cloudflare Image Resizing, et configurer les règles de cache pour que les images soient servies efficacement sans coût d'egress. Le second axe était les optimisations d'affichage côté client : galerie justifiée recalculée via ",[40,156,80],{},", progressive loading dans la lightbox (miniature floutée → full-res), et gestion cohérente du zoom\u002Fpan\u002Fswipe sur mobile sans librairie externe.",[159,160],"hr",{},[18,162,164],{"id":163},"schémas","Schémas",[166,167,169],"h3",{"id":168},"architecture-globale","Architecture globale",[10,171,172],{},[173,174],"img",{"alt":169,"src":175},"\u002Fdiagrams\u002Fportfolio-photo\u002Farchitecture.svg",[159,177],{},[166,179,181],{"id":180},"pipeline-upload-dune-photo","Pipeline upload d'une photo",[10,183,184],{},[173,185],{"alt":186,"src":187},"Pipeline upload","\u002Fdiagrams\u002Fportfolio-photo\u002Fupload.svg",[159,189],{},[166,191,193],{"id":192},"pipeline-de-delivery-des-images","Pipeline de delivery des images",[10,195,196],{},[173,197],{"alt":198,"src":199},"Pipeline delivery","\u002Fdiagrams\u002Fportfolio-photo\u002Fdelivery.svg",[159,201],{},[166,203,205],{"id":204},"structure-des-pages-routing","Structure des pages & routing",[10,207,208],{},[173,209],{"alt":210,"src":211},"Routing","\u002Fdiagrams\u002Fportfolio-photo\u002Frouting.svg",[159,213],{},[166,215,217],{"id":216},"flux-de-données-à-laffichage-dun-album","Flux de données à l'affichage d'un album",[10,219,220],{},[173,221],{"alt":222,"src":223},"Flux de données","\u002Fdiagrams\u002Fportfolio-photo\u002Fdataflow.svg",{"title":225,"searchDepth":226,"depth":226,"links":227},"",2,[228,229,230,231,232],{"id":20,"depth":226,"text":21},{"id":27,"depth":226,"text":28},{"id":84,"depth":226,"text":85},{"id":150,"depth":226,"text":151},{"id":163,"depth":226,"text":164,"children":233},[234,236,237,238,239],{"id":168,"depth":235,"text":169},3,{"id":180,"depth":235,"text":181},{"id":192,"depth":235,"text":193},{"id":204,"depth":235,"text":205},{"id":216,"depth":235,"text":217},"Portfolio photographique full-stack avec galerie justifiée, lightbox custom et back-office d'administration — pour remplacer Adobe Lightroom Web.","md",[243,244],"Répertoire Git","https:\u002F\u002Fgithub.com\u002FOwenLB\u002Fportfolio-photo",false,{},true,5,"\u002Fprojects\u002Fportfolio-photo",{"title":5,"description":240},[252,48,253,254,54,60,70],"Astro 4","TypeScript","Tailwind CSS","projects\u002Fportfolio-photo","Site full-stack","2026-07-17",[259,260],"Voir le site","https:\u002F\u002Fphoto.owenlebec.fr","FSX9HUrsXuaHTJbT69c1GKpBQ26IBp_n-6VcdolkYlg",[263,268,272,276],{"title":264,"type":265,"path":266,"order":267},"Finixa","App web & mobile","\u002Fprojects\u002Ffinixa",1,{"title":269,"type":270,"path":271,"order":226},"Peliculle","App iOS","\u002Fprojects\u002Fpeliculle",{"title":273,"type":274,"path":275,"order":235},"Scan Auto","App web","\u002Fprojects\u002Fscanauto",{"title":277,"type":278,"path":279,"order":280},"Portfolio Développeur","Site","\u002Fprojects\u002Fportfolio-dev",4,{"title":282},"Mentions légales",1784470460790]