<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dominique Megnidro</title>
    <description>The latest articles on DEV Community by Dominique Megnidro (@jmegnidro).</description>
    <link>https://dev.to/jmegnidro</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1231868%2F2f475f38-6f70-4c99-a799-e26de2924409.jpg</url>
      <title>DEV Community: Dominique Megnidro</title>
      <link>https://dev.to/jmegnidro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jmegnidro"/>
    <language>en</language>
    <item>
      <title>🚀 3 IA Integrations That Save 10 Hours/Week (And How to Replicate Them)</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Wed, 08 Oct 2025 10:34:43 +0000</pubDate>
      <link>https://dev.to/jmegnidro/3-ia-integrations-that-save-10-hoursweek-and-how-to-replicate-them-4gad</link>
      <guid>https://dev.to/jmegnidro/3-ia-integrations-that-save-10-hoursweek-and-how-to-replicate-them-4gad</guid>
      <description>&lt;p&gt;As developers and tech leads, we’re always looking for ways to automate repetitive tasks and focus on high-impact work. Here are three real-world AI integrations that saved our clients 10+ hours/week—and how you can implement them in your workflow.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Automated Meeting Summaries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before: 3 hours/week manually summarizing meeting notes.&lt;br&gt;
After: AI generates structured summaries (decisions, actions, deadlines) in 2 minutes using Otter.ai + Zapier.&lt;br&gt;
Result: +2h45/week, zero missed action items.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Smart Ticket Classification (E-commerce)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before: Manual sorting of 200+ daily support tickets (urgent, technical, billing…).&lt;br&gt;
After: NLP model (Make + Mistral AI) auto-categorizes and routes tickets.&lt;br&gt;
Result: +4h/week, zero lost tickets.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AI-Assisted Customer Responses (SaaS)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before: Copy-pasting standardized replies.&lt;br&gt;
After: AI suggests personalized responses (based on customer history + knowledge base) via Intercom + AI.&lt;br&gt;
Result: 3x faster responses, +20% customer satisfaction.&lt;/p&gt;

&lt;p&gt;How to Get Started?&lt;/p&gt;

&lt;p&gt;Pick a simple use case (e.g., FAQ chatbot to reduce support load).&lt;br&gt;
Measure before/after (e.g., resolution time, ticket volume).&lt;br&gt;
Scale to other processes.&lt;/p&gt;

&lt;p&gt;📌 Right now, we’re deploying a chatbot for a client handling 500 FAQs/month—goal: +15h/month saved.&lt;/p&gt;

&lt;p&gt;Want the Full Breakdown?&lt;br&gt;
I’ve shared detailed steps, tools, and metrics in my LinkedIn post.&lt;br&gt;
Comment “AUDIT” there, and I’ll send you a free process audit template to identify your biggest time-savers.&lt;/p&gt;

&lt;p&gt;💬 Why LinkedIn?&lt;/p&gt;

&lt;p&gt;See real client examples (screenshots, metrics).&lt;br&gt;
Get the exact phrases we used to pitch these integrations.&lt;br&gt;
Access the free audit template (only for commenters).&lt;/p&gt;

&lt;p&gt;👍 Like if you’re tired of repetitive tasks.&lt;br&gt;
🔁 Share with a teammate who needs automation.&lt;/p&gt;

</description>
      <category>linkedin</category>
      <category>ai</category>
      <category>automation</category>
      <category>saas</category>
    </item>
    <item>
      <title>Votre site charge-t-il en moins de 2 s ?</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Fri, 03 Oct 2025 21:47:40 +0000</pubDate>
      <link>https://dev.to/jmegnidro/votre-site-charge-t-il-en-moins-de-2-s--4mdd</link>
      <guid>https://dev.to/jmegnidro/votre-site-charge-t-il-en-moins-de-2-s--4mdd</guid>
      <description>&lt;p&gt;La vitesse, ce n’est pas du “confort” : c’est des conversions en plus.&lt;br&gt;
En 30 minutes, j’identifie les 5 leviers qui font baisser vos Core Web Vitals :&lt;br&gt;
Images → WebP + dimensions explicites&lt;br&gt;
Lazy-load des médias sous la ligne de flottaison&lt;br&gt;
CSS/JS critiques : minification + chargement différé&lt;br&gt;
Cache/CDN : TTL cohérents + compression&lt;br&gt;
Poids des pages : budget &amp;lt; 1,5 Mo (hors médias longs)&lt;br&gt;
👉 Résultat typique (21 jours) : LCP 4.8 s → 2.1 s, TBT 450 ms → 120 ms, CLS 0.18 → 0.04.&lt;br&gt;
Astuce du jour : passez toutes vos images en WebP et activez le lazy-load (loading="lazy").&lt;br&gt;
CTA : Demandez un check gratuit en 5 points — je vous renvoie un plan d’action concret sous 48 h.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Social Creative Coach — Multimodal content &amp; planning in one click</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Tue, 09 Sep 2025 16:17:23 +0000</pubDate>
      <link>https://dev.to/jmegnidro/social-creative-coach-multimodal-content-planning-in-one-click-3g33</link>
      <guid>https://dev.to/jmegnidro/social-creative-coach-multimodal-content-planning-in-one-click-3g33</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-ai-studio-2025-09-03"&gt;Google AI Studio Multimodal Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Social Creative Coach — Multimodal content &amp;amp; planning in one click
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Social Creative Coach&lt;/strong&gt; is a small, production-ready app that turns a brief, an image, or a short audio/video into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;channel-tailored &lt;strong&gt;post variants&lt;/strong&gt; (LinkedIn, Instagram, X, Facebook, TikTok),&lt;/li&gt;
&lt;li&gt;a 7-day &lt;strong&gt;publication schedule&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;a precise &lt;strong&gt;image prompt&lt;/strong&gt; (and optional image generations),&lt;/li&gt;
&lt;li&gt;one-click &lt;strong&gt;exports&lt;/strong&gt; (ZIP “kit”, CSV, ICS calendar, Markdown),&lt;/li&gt;
&lt;li&gt;a quick &lt;strong&gt;A/B test&lt;/strong&gt; with a scorecard and CTA recommendation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s designed for non-technical users: a clean UI with a &lt;strong&gt;cards view&lt;/strong&gt; for posts (plus a JSON view for power users). Uploading media is optional; the app can &lt;strong&gt;transcribe the first 60 seconds&lt;/strong&gt; of audio/video to seed the brief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎥 &lt;strong&gt;Video walkthrough (1 min)&lt;/strong&gt;: &lt;a href="https://youtu.be/4wix9K0JK3w" rel="noopener noreferrer"&gt;https://youtu.be/4wix9K0JK3w&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Live app (Cloud Run)&lt;/strong&gt;: &lt;a href="https://social-coach-153575963272.us-central1.run.app/" rel="noopener noreferrer"&gt;https://social-coach-153575963272.us-central1.run.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Github repository&lt;/strong&gt;: &lt;a href="https://github.com/Medogo/gemini-challenge.git" rel="noopener noreferrer"&gt;https://github.com/Medogo/gemini-challenge.git&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If Gemini 2.5 Flash Image isn’t available in your quota, the app gracefully falls back to branded placeholders, and the video shows the full flow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How I Used Google AI Studio
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Text generation&lt;/strong&gt;: &lt;code&gt;gemini-2.5-flash&lt;/code&gt; via the Gemini API (Google AI Studio) for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-channel post variants (with channel rules &amp;amp; soft limits),&lt;/li&gt;
&lt;li&gt;7-day schedule suggestions (ISO times + brief “why”),&lt;/li&gt;
&lt;li&gt;a detailed &lt;strong&gt;1080×1080 image prompt&lt;/strong&gt; tailored to brand color/name.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;(Optional) Image generation&lt;/strong&gt;: configurable &lt;code&gt;GEMINI_IMAGE_MODEL&lt;/code&gt; (e.g., &lt;code&gt;gemini-2.5-flash-image-preview&lt;/code&gt;) for producing &lt;strong&gt;image variants per post&lt;/strong&gt;. If not available, the app returns &lt;strong&gt;placeholder PNGs&lt;/strong&gt; to preserve the UX.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Multimodal input&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt; provide context and can be auto-analyzed to bootstrap a brief when the text is empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio/Video&lt;/strong&gt; get trimmed to &lt;strong&gt;60s&lt;/strong&gt; with &lt;code&gt;ffmpeg&lt;/code&gt;, converted to mono 16 kHz WAV, then transcribed (used to enrich or replace the brief).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The app runs on &lt;strong&gt;Cloud Run&lt;/strong&gt;; secrets (Gemini API key) are stored in &lt;strong&gt;Secret Manager&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multimodal Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input&lt;/strong&gt;: text brief, image, or short audio/video (60s excerpt for speed &amp;amp; cost).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image analysis&lt;/strong&gt;: caption, objects, colors, style, product, mood.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand kit hints&lt;/strong&gt;: &lt;code&gt;brand_name&lt;/code&gt; &amp;amp; &lt;code&gt;brand_color&lt;/code&gt; injected into post and image prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image variants per post&lt;/strong&gt;: &lt;code&gt;/images/zip&lt;/code&gt; can generate multiple images for each post variant; supports an optional &lt;strong&gt;style reference&lt;/strong&gt; upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A/B test&lt;/strong&gt;: generates A &amp;amp; B for a chosen channel and returns a &lt;strong&gt;scorecard&lt;/strong&gt; + recommended CTA.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Exports&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ZIP Kit&lt;/strong&gt; with variants/schedule/image prompt (+ README.md),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ICS&lt;/strong&gt; calendar events,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV&lt;/strong&gt; for post ops,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown&lt;/strong&gt; snapshot.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  UX Highlights
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cards view&lt;/strong&gt; for human-readable posts (titles, body, hashtags, CTA) with copy-to-clipboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON view&lt;/strong&gt; for raw output + copy button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Quick Example”&lt;/strong&gt; button: instantly pre-fills the form; optional sample image/audio files are auto-loaded.&lt;/li&gt;
&lt;li&gt;Clear status messages, file size limits (20 MB image / 100 MB media), and &lt;strong&gt;60s request timeout&lt;/strong&gt; on the frontend to avoid hangs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture (High-Level)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Static HTML/CSS/JS (no framework) served by FastAPI’s &lt;code&gt;StaticFiles&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: FastAPI + Gemini SDK (text &amp;amp; optional image models), &lt;code&gt;pydub&lt;/code&gt; + &lt;code&gt;ffmpeg&lt;/code&gt; for media trimming, &lt;code&gt;speech_recognition&lt;/code&gt; for transcription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt;: Docker → Cloud Build → &lt;strong&gt;Cloud Run&lt;/strong&gt; (with min instance for warm starts); &lt;strong&gt;Secret Manager&lt;/strong&gt; for API key; &lt;strong&gt;CORS&lt;/strong&gt; open for demo; favicon + samples to keep logs clean and make the demo smooth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Multimodal Matters Here
&lt;/h2&gt;

&lt;p&gt;Marketing teams rarely start from a clean text brief. They have &lt;strong&gt;assets&lt;/strong&gt;: a product photo, a CEO voice note, a teaser clip. Letting users drop &lt;strong&gt;any&lt;/strong&gt; of those in and still get coherent text, a schedule, and images removes friction and showcases the power of &lt;strong&gt;Google AI Studio’s multimodal stack&lt;/strong&gt; in a practical, demo-able way.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📺 Video: &lt;a href="https://youtu.be/4wix9K0JK3w" rel="noopener noreferrer"&gt;https://youtu.be/4wix9K0JK3w&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 Live app: &lt;a href="https://social-coach-153575963272.us-central1.run.app/" rel="noopener noreferrer"&gt;https://social-coach-153575963272.us-central1.run.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Github repository&lt;/strong&gt;: &lt;a href="https://github.com/Medogo/gemini-challenge.git" rel="noopener noreferrer"&gt;https://github.com/Medogo/gemini-challenge.git&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;(If the image model quota is unavailable, the demo still runs end-to-end with branded placeholders so judges can see the full flow.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>ai</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Marketing pour développeurs : La compétence que vous n'apprenez pas en codant</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Tue, 02 Sep 2025 18:43:58 +0000</pubDate>
      <link>https://dev.to/jmegnidro/marketing-pour-developpeurs-la-competence-que-vous-napprenez-pas-en-codant-k3g</link>
      <guid>https://dev.to/jmegnidro/marketing-pour-developpeurs-la-competence-que-vous-napprenez-pas-en-codant-k3g</guid>
      <description>&lt;p&gt;En tant que développeurs, on est formés à résoudre des problèmes complexes avec du code. On se concentre sur les algorithmes, l'optimisation et la création de produits solides. On aime construire. Mais une fois le produit parfait achevé, une question cruciale se pose : comment le monde entier peut-il en profiter si personne ne sait qu'il existe ?&lt;/p&gt;

&lt;p&gt;C'est là que le marketing digital entre en jeu. Ce n'est pas un concept abstrait réservé aux "spécialistes". C'est un ensemble de stratégies pratiques qui, lorsqu'elles sont bien comprises, peuvent propulser votre projet du simple hobby au succès.&lt;/p&gt;

&lt;p&gt;Mots-clés : Identifiez les termes que votre public cible recherche. Utilisez des outils comme Ubersuggest ou Google Trends pour trouver des mots-clés pertinents.&lt;/p&gt;

&lt;p&gt;Liens : Intégrez des liens internes vers d'autres pages de votre site, et essayez d'obtenir des liens d'autres sites web de confiance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Créez du contenu qui attire les gens
&lt;/h2&gt;

&lt;p&gt;Le meilleur moyen d'attirer l'attention est de créer du contenu qui a de la valeur pour votre public cible. Pensez à votre produit. À quel problème répond-il ?&lt;/p&gt;

&lt;p&gt;Blog Post : Rédigez des articles sur les défis que votre produit résout. Par exemple, si vous avez créé une API de gestion de données, écrivez un guide sur les "5 erreurs à éviter dans l'intégration d'API".&lt;/p&gt;

&lt;p&gt;Tutoriels : Montrez comment votre outil simplifie une tâche courante. Les développeurs adorent les tutoriels étape par étape.&lt;/p&gt;

&lt;p&gt;Études de cas : Partagez le succès de vos premiers utilisateurs. Comment votre produit a-t-il amélioré leur travail ?&lt;/p&gt;

&lt;h2&gt;
  
  
  Le SEO n'est pas une magie noire
&lt;/h2&gt;

&lt;p&gt;Le SEO (Search Engine Optimization) n'est pas de la sorcellerie. C'est de l'ingénierie appliquée au web. Il s'agit simplement de rendre votre contenu facile à trouver pour les moteurs de recherche comme Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les réseaux sociaux sont votre meilleur ami
&lt;/h2&gt;

&lt;p&gt;Vous êtes déjà sur des plateformes comme LinkedIn ou X (Twitter). Utilisez-les !&lt;/p&gt;

&lt;p&gt;Partagez vos articles : Ne vous contentez pas de poster un lien. Posez des questions, lancez des débats.&lt;/p&gt;

&lt;p&gt;Participez aux conversations : Suivez les hashtags pertinents (#buildinpublic, #webdev, #API) et interagissez avec d'autres développeurs. La visibilité est une question de communauté.&lt;/p&gt;

&lt;p&gt;Le marketing n'est pas une distraction du développement ; c'est une extension de celui-ci. C'est l'art de communiquer la valeur que vous avez si durement créée. Commencez petit, apprenez en faisant et vous verrez que vos projets ne seront pas seulement techniquement brillants, mais aussi largement adoptés.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>🚀 Lancement de Smart Pilote : l’automatisation sécurisée de vos contenus réseaux sociaux</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Sat, 23 Aug 2025 12:07:51 +0000</pubDate>
      <link>https://dev.to/jmegnidro/lancement-de-smart-pilote-lautomatisation-securisee-de-vos-contenus-reseaux-sociaux-4422</link>
      <guid>https://dev.to/jmegnidro/lancement-de-smart-pilote-lautomatisation-securisee-de-vos-contenus-reseaux-sociaux-4422</guid>
      <description>&lt;p&gt;Bonjour la communauté 👋&lt;/p&gt;

&lt;p&gt;Je suis ravi de vous annoncer le lancement de Smart Pilote, une solution SaaS que nous développons pour aider entrepreneurs, créateurs et entreprises à automatiser leurs contenus sur les réseaux sociaux, en toute simplicité et sécurité.&lt;/p&gt;

&lt;p&gt;🌍 Le problème&lt;/p&gt;

&lt;p&gt;Aujourd’hui, beaucoup d’entreprises et de créateurs passent des heures à planifier, rédiger et publier leurs posts.&lt;br&gt;
Résultat :&lt;/p&gt;

&lt;p&gt;du temps perdu,&lt;/p&gt;

&lt;p&gt;des opportunités manquées,&lt;/p&gt;

&lt;p&gt;et une régularité difficile à maintenir.&lt;/p&gt;

&lt;p&gt;À cela s’ajoute une difficulté importante : la gestion des accès aux comptes. Partager ses identifiants n’est ni sécurisé, ni pratique.&lt;/p&gt;

&lt;p&gt;💡 Notre solution : Smart Pilote&lt;/p&gt;

&lt;p&gt;Avec Smart Pilote, vous pouvez :&lt;br&gt;
✅ Créer rapidement vos contenus (assistés par IA si besoin),&lt;br&gt;
✅ Planifier et automatiser vos publications sur plusieurs réseaux,&lt;br&gt;
✅ Analyser la performance de vos posts,&lt;br&gt;
✅ Collaborer facilement avec votre équipe,&lt;br&gt;
✅ Inviter des gestionnaires sans partager vos credentials : un simple mail d’invitation suffit pour connecter un compte en toute sécurité.&lt;/p&gt;

&lt;p&gt;Notre objectif est simple : vous libérer du temps, renforcer la sécurité et vous aider à rester visible en ligne sans effort.&lt;/p&gt;

&lt;p&gt;🛠️ Fonctionnalités clés (MVP)&lt;/p&gt;

&lt;p&gt;Multi-réseaux sociaux (Facebook, LinkedIn, Instagram, X, etc.)&lt;/p&gt;

&lt;p&gt;Automatisation intelligente&lt;/p&gt;

&lt;p&gt;Tableau de bord clair et minimaliste&lt;/p&gt;

&lt;p&gt;Gestion multi-utilisateurs et rôles&lt;/p&gt;

&lt;p&gt;🔐 Connexion sécurisée par invitation (sans partage de credentials)&lt;/p&gt;

&lt;p&gt;🙌 Pourquoi Dev.to ?&lt;/p&gt;

&lt;p&gt;Nous savons que beaucoup ici construisent des produits, et nous aimerions :&lt;/p&gt;

&lt;p&gt;partager notre expérience de développement,&lt;/p&gt;

&lt;p&gt;recueillir vos feedbacks,&lt;/p&gt;

&lt;p&gt;apprendre de vos propres parcours SaaS.&lt;/p&gt;

&lt;p&gt;🚀 Et la suite ?&lt;/p&gt;

&lt;p&gt;👉 Nous lançons actuellement notre phase bêta et cherchons nos premiers utilisateurs-testeurs.&lt;br&gt;
Si vous êtes intéressé(e), vous pouvez le tester sur frontoffice.smartpilote.com.&lt;/p&gt;

&lt;p&gt;Merci d’avance pour vos retours, ils nous aideront à améliorer Smart Pilote et à en faire un outil vraiment utile pour la communauté 💙&lt;/p&gt;

&lt;p&gt;À très vite,&lt;br&gt;
L’équipe Smart Pilote ✨&lt;/p&gt;

</description>
      <category>linkedin</category>
      <category>chatgpt</category>
      <category>saas</category>
      <category>startup</category>
    </item>
    <item>
      <title>How I Connected Django to AWS S3: Full Story, Real Errors, and the Final Working Setup</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Sat, 26 Apr 2025 14:56:02 +0000</pubDate>
      <link>https://dev.to/jmegnidro/full-article-in-english-copy-paste-ready-5g1j</link>
      <guid>https://dev.to/jmegnidro/full-article-in-english-copy-paste-ready-5g1j</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Deploying a Django app with AWS S3 for &lt;strong&gt;media files&lt;/strong&gt; and &lt;strong&gt;static files&lt;/strong&gt; seems like a simple checklist item at first glance.&lt;/p&gt;

&lt;p&gt;But when you actually get your hands dirty, things quickly spiral out of control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access Denied&lt;/strong&gt; errors,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ACL&lt;/strong&gt; configuration nightmares,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucket Policies&lt;/strong&gt; blocking everything,&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;broken Django Admin&lt;/strong&gt; with no CSS,&lt;/li&gt;
&lt;li&gt;Silent upload failures,&lt;/li&gt;
&lt;li&gt;And general confusion over how public access actually works in S3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a developer, I wanted to make my project &lt;strong&gt;more scalable&lt;/strong&gt; and &lt;strong&gt;more professional&lt;/strong&gt; by hosting all my &lt;strong&gt;images&lt;/strong&gt;, &lt;strong&gt;CSS&lt;/strong&gt;, and &lt;strong&gt;JavaScript&lt;/strong&gt; assets on AWS S3 instead of keeping everything on the local server.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;full journey&lt;/strong&gt; — the mistakes, the fixes, and the lessons I learned — to build a &lt;strong&gt;reliable, fast, and secure&lt;/strong&gt; integration between Django and AWS S3.&lt;/p&gt;

&lt;p&gt;If you're planning to connect Django to S3, this guide is for you — no sugarcoating, real-world problems included.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Creating the S3 Bucket
&lt;/h2&gt;

&lt;p&gt;First step: create a new S3 bucket.&lt;/p&gt;

&lt;p&gt;AWS Console ➔ S3 ➔ Create bucket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First trap:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
AWS &lt;strong&gt;by default recommends blocking all public access&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Big mistake.&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you block everything, even your own frontend, Django, and yourself won't be able to access the files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real solution:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Keep &lt;strong&gt;\"Block public access to new and existing ACLs\"&lt;/strong&gt; checked.&lt;/li&gt;
&lt;li&gt;❌ Uncheck &lt;strong&gt;\"Block public access via new bucket policies\"&lt;/strong&gt; and &lt;strong&gt;\"Block public access via any bucket policies\"&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, you can control public access &lt;strong&gt;only&lt;/strong&gt; through a &lt;strong&gt;Bucket Policy&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Setting the Bucket Policy
&lt;/h2&gt;

&lt;p&gt;Next, you need to &lt;strong&gt;explicitly allow public read access&lt;/strong&gt; using a Bucket Policy.&lt;/p&gt;

&lt;p&gt;Here’s the policy I applied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowPublicReadAccessToObjects"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::your-bucket-name/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ This allows &lt;strong&gt;everyone to read&lt;/strong&gt; your files,&lt;br&gt;&lt;br&gt;
❌ but &lt;strong&gt;nobody&lt;/strong&gt; can upload, modify, or delete files unless you authorize them.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Creating an IAM User and Access Keys
&lt;/h2&gt;

&lt;p&gt;Next step: create an IAM user dedicated to accessing S3 from your Django app.&lt;/p&gt;

&lt;p&gt;In AWS Console ➔ IAM ➔ Users ➔ Create user:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Programmatic access ✅&lt;/li&gt;
&lt;li&gt;Attach policy ➔ &lt;strong&gt;AmazonS3FullAccess&lt;/strong&gt; (for setup, then you can restrict later)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save your:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AWS_ACCESS_KEY_ID&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS_SECRET_ACCESS_KEY&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You will need them in your Django settings.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Configuring Django to Use S3
&lt;/h2&gt;

&lt;p&gt;Inside your &lt;code&gt;settings.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; 
&lt;span class="n"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;AWS_STORAGE_BUCKET_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-bucket-name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;AWS_S3_REGION_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eu-north-1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;AWS_S3_CUSTOM_DOMAIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AWS_STORAGE_BUCKET_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.s3.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AWS_S3_REGION_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.amazonaws.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="n"&gt;STATICFILES_STORAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;storage.StaticStorage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;DEFAULT_FILE_STORAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;storage.MediaStorage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="n"&gt;STATIC_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AWS_S3_CUSTOM_DOMAIN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/static/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;MEDIA_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AWS_S3_CUSTOM_DOMAIN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/media/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;storage.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;storages.backends.s3boto3&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;S3Boto3Storage&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StaticStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;S3Boto3Storage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;static&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MediaStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;S3Boto3Storage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;media&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Now Django will &lt;strong&gt;upload and fetch&lt;/strong&gt; media and static files directly from S3.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Real Problems I Faced and How I Solved Them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: AccessDenied when trying to read files&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Solved by correctly setting the Bucket Policy.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Problem 2: AccessControlListNotSupported error during upload&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My bucket had \"Bucket owner enforced\" enabled, meaning &lt;strong&gt;ACLs are not allowed&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;✅ Solution: &lt;strong&gt;do not use &lt;code&gt;--acl public-read&lt;/code&gt;&lt;/strong&gt; when uploading with AWS CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, simply upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;media/ s3://your-bucket-name/media/ &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Problem 3: Django Admin with no CSS, no JavaScript&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Because static files were not uploaded to S3 yet.&lt;/li&gt;
&lt;li&gt;✅ Solution:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  python manage.py collectstatic
  aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;staticfiles/ s3://your-bucket-name/static/ &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Always upload from &lt;code&gt;staticfiles/&lt;/code&gt;, not from &lt;code&gt;static/&lt;/code&gt;.)&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Automating Static Uploads with a Simple Bash Script
&lt;/h2&gt;

&lt;p&gt;I created a simple &lt;code&gt;upload_static.sh&lt;/code&gt; script to save time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"📦 Collecting static files..."&lt;/span&gt;
python manage.py collectstatic &lt;span class="nt"&gt;--noinput&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"☁️ Uploading static files to AWS S3..."&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;staticfiles/ s3://your-bucket-name/static/ &lt;span class="nt"&gt;--recursive&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ Upload complete!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x upload_static.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./upload_static.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and everything gets updated in seconds 🚀.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Connecting Django to AWS S3 &lt;strong&gt;seems easy&lt;/strong&gt;, but the devil is in the details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understanding how &lt;strong&gt;S3 public access&lt;/strong&gt; really works,&lt;/li&gt;
&lt;li&gt;Managing &lt;strong&gt;Bucket Policies vs ACLs&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;Keeping your &lt;strong&gt;static&lt;/strong&gt; and &lt;strong&gt;media&lt;/strong&gt; files organized,&lt;/li&gt;
&lt;li&gt;Anticipating &lt;strong&gt;AccessDenied&lt;/strong&gt; and &lt;strong&gt;ACL errors&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;Automating your deployment process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By learning through real mistakes and solving them, I made my project &lt;strong&gt;faster&lt;/strong&gt;, &lt;strong&gt;scalable&lt;/strong&gt;, and &lt;strong&gt;more secure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're building a Django project and aiming for professional-grade deployment,&lt;br&gt;&lt;br&gt;
&lt;strong&gt;I hope this guide saves you hours (if not days) of frustration.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Good luck with your project 🚀👨‍💻 — and stay strong if AWS throws some surprises your way!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>beginners</category>
      <category>ai</category>
    </item>
    <item>
      <title>J'ai failli abandonner l'intégration Django + S3 (et toi aussi si tu rates ça)</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Sat, 26 Apr 2025 14:47:20 +0000</pubDate>
      <link>https://dev.to/jmegnidro/jai-failli-abandonner-lintegration-django-s3-et-toi-aussi-si-tu-rates-ca-2pib</link>
      <guid>https://dev.to/jmegnidro/jai-failli-abandonner-lintegration-django-s3-et-toi-aussi-si-tu-rates-ca-2pib</guid>
      <description>&lt;p&gt;Déployer une application Django qui utilise AWS S3 pour gérer ses fichiers médias et fichiers statiques semble, au premier abord, une formalité.&lt;/p&gt;

&lt;p&gt;Pourtant, quand on se lance vraiment, les surprises arrivent vite :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;erreurs Access Denied,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;problèmes avec les ACL,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bucket Policies trop restrictives,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tableau de bord Django sans style CSS,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;erreurs silencieuses à l'upload,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;incompréhension de la gestion de l'accès public sur S3.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En tant que développeur, je voulais rendre mon projet plus scalable, plus propre, en hébergeant tous mes médias et fichiers statiques sur AWS S3, plutôt que de tout laisser sur le serveur local.&lt;/p&gt;

&lt;p&gt;Je partage ici tout mon cheminement, mes erreurs, et surtout comment je les ai corrigées, pour obtenir une intégration fiable, rapide, et sécurisée.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Création du Bucket S3
&lt;/h2&gt;

&lt;p&gt;Première étape, créer un bucket S3.&lt;/p&gt;

&lt;p&gt;Je me connecte à AWS Management Console &amp;gt; S3 &amp;gt; Créer un bucket.&lt;/p&gt;

&lt;p&gt;Piège n°1 : les paramètres de blocage d'accès public&lt;/p&gt;

&lt;p&gt;Par défaut, AWS recommande de bloquer tous les accès publics. Ce que j'ai fait.&lt;/p&gt;

&lt;p&gt;Problème ?&lt;/p&gt;

&lt;p&gt;Si tout est bloqué, Django, mon frontend et même moi n'avons plus accès aux fichiers !&lt;/p&gt;

&lt;p&gt;Solution :&lt;/p&gt;

&lt;p&gt;Je laisse coché "Bloquer toutes les ACL (anciennes et nouvelles)" ✅.&lt;/p&gt;

&lt;p&gt;Mais je décoche "Bloquer l'accès public via Bucket Policies" ❌.&lt;/p&gt;

&lt;p&gt;Ainsi, je gère tout l'accès public à travers une Bucket Policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Configuration de la Bucket Policy
&lt;/h2&gt;

&lt;p&gt;Deuxième étape, permettre l'accès en lecture publique.&lt;/p&gt;

&lt;p&gt;Je mets cette Bucket Policy sur mon bucket :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPublicReadAccessToObjects",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::mon-bucket-name/*"
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Création de l'utilisateur IAM et des clés d'accès
&lt;/h2&gt;

&lt;p&gt;Je crée un utilisateur IAM dans AWS Console :&lt;/p&gt;

&lt;p&gt;Accès programmatique uniquement.&lt;/p&gt;

&lt;p&gt;Permission AmazonS3FullAccess (temporairement pour tout mettre en place).&lt;/p&gt;

&lt;p&gt;Je récupère :&lt;/p&gt;

&lt;p&gt;AWS_ACCESS_KEY_ID&lt;/p&gt;

&lt;p&gt;AWS_SECRET_ACCESS_KEY&lt;/p&gt;

&lt;p&gt;À mettre ensuite dans Django.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Configuration Django pour utiliser S3
&lt;/h2&gt;

&lt;p&gt;Dans ton settings :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_ACCESS_KEY_ID = '...'
AWS_SECRET_ACCESS_KEY = '...'
AWS_STORAGE_BUCKET_NAME = 'mon-bucket-name'
AWS_S3_REGION_NAME = 'eu-north-1'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.amazonaws.com'

STATICFILES_STORAGE = 'storage.StaticStorage'
DEFAULT_FILE_STORAGE = 'storage.MediaStorage'

STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/static/'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dans ton fichier storage :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from storages.backends.s3boto3 import S3Boto3Storage

class StaticStorage(S3Boto3Storage):
    location = 'static'
    default_acl = 'public-read'

class MediaStorage(S3Boto3Storage):
    location = 'media'
    default_acl = 'public-read'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tu crée ton fichier upload_media pour ne pas ecraser tes fichiers existant&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import boto3
import os
from django.conf import settings
from django.core.management.base import BaseCommand


def upload_media_to_s3():
    s3_client = boto3.client(
        's3',
        aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
        aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
        region_name=settings.AWS_S3_REGION_NAME
    )

    media_root = settings.MEDIA_ROOT
    for root, dirs, files in os.walk(media_root):
        for file in files:
            local_path = os.path.join(root, file)
            relative_path = os.path.relpath(local_path, media_root)
            s3_path = os.path.join('media', relative_path)

            s3_client.upload_file(
                local_path,
                settings.AWS_STORAGE_BUCKET_NAME,
                s3_path
            )
            print(f"Uploaded {local_path} to {s3_path}")


if __name__ == "__main__":
    import django

    django.setup()
    upload_media_to_s3()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Problèmes rencontrés et solutions
&lt;/h2&gt;

&lt;p&gt;Problème 1 : AccessDenied en accédant aux fichiers&lt;/p&gt;

&lt;p&gt;Corrigé en mettant une Bucket Policy propre.&lt;/p&gt;

&lt;p&gt;Problème 2 : AccessControlListNotSupported&lt;/p&gt;

&lt;p&gt;Mon bucket était configuré avec "Bucket owner enforced".&lt;/p&gt;

&lt;p&gt;J'ai supprimé l'option --acl public-read dans AWS CLI.&lt;/p&gt;

&lt;p&gt;Problème 3 : tableau de bord Django sans CSS&lt;/p&gt;

&lt;p&gt;J'ai oublié d'envoyer mes fichiers staticfiles/ sur S3.&lt;/p&gt;

&lt;p&gt;Solution :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py collectstatic
aws s3 cp staticfiles/ s3://mon-bucket-name/static/ --recursive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Script automatique pour uploader les fichiers statiques
&lt;/h2&gt;

&lt;p&gt;J'ai créé un petit script Bash upload_static.sh :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

python manage.py collectstatic --noinput
aws s3 cp staticfiles/ s3://mon-bucket-name/static/ --recursive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exécution :&lt;br&gt;
&lt;code&gt;./upload_static.sh&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Simple et efficace !&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Intégrer Django avec AWS S3 demande de comprendre quelques subtilités :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Comment S3 gère l'accès public,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Comment fonctionnent les ACL et Bucket Policies,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Comment organiser ses fichiers statiques et médias,&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comment anticiper les erreurs AccessDenied et AccessControlListNotSupported.
&lt;/h2&gt;

&lt;p&gt;Grâce à ce parcours et à ces solutions, j'ai pu rendre mon application plus rapide, plus robuste et prête à scaler.&lt;/p&gt;

&lt;p&gt;Si toi aussi tu construis ton projet Django sur AWS, j'espère que ce guide t'évitera de nombreuses heures de galère ! 🚀&lt;/p&gt;

&lt;p&gt;Bon déploiement à toi 👋 !&lt;/p&gt;

</description>
      <category>aws</category>
      <category>django</category>
      <category>fastapi</category>
      <category>automation</category>
    </item>
    <item>
      <title>Les optimisations Django méconnues qui peuvent réduire le temps d'exécution de vos requêtes de 90%</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Thu, 17 Apr 2025 16:21:03 +0000</pubDate>
      <link>https://dev.to/jmegnidro/les-optimisations-django-meconnues-qui-peuvent-reduire-le-temps-dexecution-de-vos-requetes-de-90-1p0b</link>
      <guid>https://dev.to/jmegnidro/les-optimisations-django-meconnues-qui-peuvent-reduire-le-temps-dexecution-de-vos-requetes-de-90-1p0b</guid>
      <description>&lt;p&gt;Salut la communauté DEV ! 👋&lt;br&gt;
Aujourd'hui, je vais partager avec vous quelques astuces d'optimisation Django qui surprennent même les développeurs seniors. Si vous utilisez encore Django comme une simple couche d'abstraction sur votre base de données, vous pourriez passer à côté d'un potentiel d'optimisation énorme.&lt;/p&gt;
&lt;h2&gt;
  
  
  Le problème classique
&lt;/h2&gt;

&lt;p&gt;Trop souvent, nous écrivons du code comme celui-ci :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# On récupère toutes les données puis on traite en Python 🐌
users = User.objects.all()
active_premium_users = [user for user in users 
                         if user.is_active and user.subscription_level == 'premium' 
                         and user.last_login &amp;gt; timezone.now() - timedelta(days=30)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce code fonctionne, mais il souffre d'un problème majeur : il récupère TOUTES les données avant de filtrer. Pour 1000 utilisateurs, c'est 1000 objets Python créés, même si seulement 50 correspondent à vos critères.&lt;/p&gt;

&lt;h2&gt;
  
  
  La puissance cachée des fonctions de base de données
&lt;/h2&gt;

&lt;p&gt;Django offre des fonctionnalités avancées qui permettent de déléguer ce travail directement à la base de données. Voici comment transformer l'exemple ci-dessus :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.db.models import F, ExpressionWrapper, DateTimeField
from django.db.models.functions import Now

# La base de données fait tout le travail 🚀
thirty_days_ago = ExpressionWrapper(Now() - timedelta(days=30), output_field=DateTimeField())
active_premium_users = User.objects.filter(
    is_active=True,
    subscription_level='premium',
    last_login__gt=thirty_days_ago
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cette approche peut réduire votre temps d'exécution de 90% sur des jeux de données importants !&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculs agrégés conditionnels
&lt;/h2&gt;

&lt;p&gt;Voici un autre cas où les optimisations brillent :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Avant : Calculs manuels en Python 😓
orders = Order.objects.all()
total_amount = 0
premium_amount = 0
for order in orders:
    total_amount += order.amount
    if order.user.subscription_level == 'premium':
        premium_amount += order.amount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transformons cela en une seule requête efficace :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.db.models import Sum, Case, When, DecimalField

# Après : Une seule requête SQL ⚡
result = Order.objects.aggregate(
    total_amount=Sum('amount'),
    premium_amount=Sum(Case(
        When(user__subscription_level='premium', then=F('amount')),
        default=0,
        output_field=DecimalField()
    ))
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fonctions personnalisées pour des besoins spécifiques
&lt;/h2&gt;

&lt;p&gt;Vous pouvez même créer vos propres fonctions SQL :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.db.models import Func

class LevenshteinDistance(Func):
    function = 'LEVENSHTEIN'  # Fonction PostgreSQL

# Recherche floue ultra-rapide
similar_products = Product.objects.annotate(
    name_similarity=LevenshteinDistance('name', Value('iphone'))
).filter(
    name_similarity__lte=3
).order_by('name_similarity')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Optimisation des calculs géographiques
&lt;/h2&gt;

&lt;p&gt;Les calculs géospatiaux sont particulièrement coûteux en Python :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.contrib.postgres.fields import ArrayField
from django.db.models import F, FloatField, ExpressionWrapper, Func, Value

# Calcul de distance dans la DB plutôt qu'en Python
nearby_stores = Store.objects.annotate(
    distance=ExpressionWrapper(
        Func(F('location'), Value(user_location), function='ST_Distance'),
        output_field=FloatField()
    )
).filter(distance__lte=5000).order_by('distance')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quelques conseils pour maximiser les performances
&lt;/h2&gt;

&lt;p&gt;Profilez d'abord : Utilisez django-debug-toolbar pour identifier les requêtes lentes&lt;br&gt;
Pensez SQL : Si votre opération peut s'exprimer en SQL, elle peut probablement être optimisée&lt;br&gt;
Utilisez les annotations : Elles permettent de calculer des valeurs à la volée sans créer de modèles supplémentaires&lt;br&gt;
Exploitez les fonctions spécifiques à votre moteur de base de données (PostgreSQL, MySQL...)&lt;/p&gt;

&lt;p&gt;Conclusion&lt;br&gt;
Ces techniques peuvent sembler complexes au premier abord, mais elles valent vraiment la peine d'être maîtrisées. J'ai vu des API passer de plusieurs secondes à quelques dizaines de millisecondes grâce à ces optimisations.&lt;br&gt;
N'hésitez pas à partager vos propres astuces d'optimisation Django dans les commentaires !&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Email Scraping: Techniques and Ethical Considerations with Python</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Wed, 19 Mar 2025 12:36:09 +0000</pubDate>
      <link>https://dev.to/jmegnidro/email-scraping-techniques-and-ethical-considerations-with-python-223c</link>
      <guid>https://dev.to/jmegnidro/email-scraping-techniques-and-ethical-considerations-with-python-223c</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Email scraping tools have become an essential component for many businesses and marketing professionals. Whether for prospecting, contact research, or lead generation, these tools automate the collection of contact information from the web. This article presents the fundamental principles of email scraping, with a practical Python example.&lt;br&gt;
What is email scraping?&lt;br&gt;
Email scraping involves automatically extracting email addresses and other contact information (names, phone numbers, etc.) from websites. This technique allows for rapidly building databases of prospects or professional contacts.&lt;br&gt;
Ethical and legal considerations&lt;br&gt;
Before creating or using an email scraper, it's crucial to consider several aspects:&lt;/p&gt;

&lt;p&gt;GDPR compliance: In Europe, the General Data Protection Regulation imposes strict restrictions on the collection and use of personal data.&lt;br&gt;
Respect for terms of service of visited websites&lt;br&gt;
Observance of robots.txt files which indicate areas forbidden to scraping&lt;br&gt;
Technical limitations implemented by websites (CAPTCHA, request limits, etc.)&lt;/p&gt;

&lt;p&gt;Practical example: an email scraper in Python&lt;br&gt;
Here's a Python code example that illustrates the basic principles of email scraping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import requests
from bs4 import BeautifulSoup
import re
import csv
import time
import concurrent.futures
from urllib.parse import urlparse, urljoin
import argparse


class EmailScraper:
    def __init__(self, max_pages=5, max_depth=2, delay=1, threads=5):
        self.max_pages = max_pages  # Maximum de pages à explorer par domaine
        self.max_depth = max_depth  # Profondeur maximale de crawling
        self.delay = delay  # Délai entre les requêtes
        self.threads = threads  # Nombre de threads pour le traitement parallèle
        self.visited_urls = set()  # URLs déjà visitées

        # User-Agent aléatoires pour éviter les blocages
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }

        # Patterns pour la recherche
        self.email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
        self.phone_pattern = r'(?:\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}'
        self.name_pattern = r'(?:Contact|About|Team|Staff).*?(?:&amp;lt;h\d&amp;gt;)(.*?)(?:&amp;lt;/h\d&amp;gt;)'

    def is_valid_url(self, url, base_domain):
        """Vérifie si l'URL appartient au même domaine"""
        try:
            parsed_url = urlparse(url)
            parsed_base = urlparse(base_domain)
            return parsed_url.netloc == parsed_base.netloc or parsed_url.netloc == ''
        except:
            return False

    def get_base_url(self, url):
        """Extrait l'URL de base d'un site"""
        parsed = urlparse(url)
        return f"{parsed.scheme}://{parsed.netloc}"

    def normalize_url(self, url, base_url):
        """Normalise les URLs relatives"""
        if not url:
            return None
        if url.startswith(('http://', 'https://')):
            return url
        return urljoin(base_url, url)

    def extract_company_info(self, soup, url):
        """Tente d'extraire le nom de l'entreprise"""
        company_name = ""

        # Essaie de trouver le nom via le title
        if soup.title:
            title = soup.title.string
            if title:
                company_name = title.split('|')[0].split('-')[0].strip()

        # Essaie de trouver via les métadonnées
        meta_og_site_name = soup.find('meta', property='og:site_name')
        if meta_og_site_name and meta_og_site_name.get('content'):
            company_name = meta_og_site_name.get('content')

        # Si on n'a pas trouvé de nom, utiliser le domaine
        if not company_name:
            domain = urlparse(url).netloc
            company_name = domain.replace('www.', '').split('.')[0].capitalize()

        return company_name

    def extract_data_from_page(self, url, base_url, depth=0):
        """Extrait les données d'une page et retourne les liens pour crawling"""
        if url in self.visited_urls or depth &amp;gt;= self.max_depth:
            return [], {}

        self.visited_urls.add(url)
        print(f"Traitement de: {url}")

        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            if response.status_code != 200:
                return [], {}

            soup = BeautifulSoup(response.text, 'html.parser')
            page_text = soup.get_text()

            # Extraction des emails
            emails = set(re.findall(self.email_pattern, page_text))
            for link in soup.find_all('a'):
                href = link.get('href', '')
                if 'mailto:' in href:
                    email = href.split('mailto:')[1].split('?')[0]
                    emails.add(email)

            # Extraction des téléphones
            phones = set(re.findall(self.phone_pattern, page_text))

            # Extraction des noms (simple - peut être améliorée)
            names = []
            for contact_section in soup.find_all(['div', 'section'], class_=lambda c: c and (
                    'contact' in c.lower() or 'team' in c.lower())):
                person_elements = contact_section.find_all(['h2', 'h3', 'h4', 'strong'])
                for element in person_elements:
                    if element.text and len(element.text.strip()) &amp;lt; 50:  # Éviter les faux positifs
                        names.append(element.text.strip())

            # Si on est sur la page d'accueil, essayer d'extraire le nom de l'entreprise
            company_name = ""
            if depth == 0 or "about" in url.lower() or "contact" in url.lower():
                company_name = self.extract_company_info(soup, url)

            # Trouver d'autres liens à explorer
            links_to_follow = []
            if len(self.visited_urls) &amp;lt; self.max_pages:
                for link in soup.find_all('a'):
                    href = link.get('href')
                    if href:
                        normalized_url = self.normalize_url(href, base_url)
                        if normalized_url and self.is_valid_url(normalized_url,
                                                                base_url) and normalized_url not in self.visited_urls:
                            # Prioriser les pages de contact
                            if 'contact' in normalized_url.lower():
                                links_to_follow.insert(0, normalized_url)
                            else:
                                links_to_follow.append(normalized_url)

            data = {
                'url': url,
                'company_name': company_name,
                'emails': list(emails),
                'phones': list(phones),
                'names': names
            }

            return links_to_follow, data

        except Exception as e:
            print(f"Erreur sur {url}: {e}")
            return [], {}

    def crawl_website(self, start_url):
        """Fonction principale pour explorer un site web"""
        if not start_url.startswith(('http://', 'https://')):
            start_url = 'https://' + start_url

        base_url = self.get_base_url(start_url)
        pages_to_visit = [start_url]
        collected_data = []

        while pages_to_visit and len(self.visited_urls) &amp;lt; self.max_pages:
            current_url = pages_to_visit.pop(0)
            links, data = self.extract_data_from_page(current_url, base_url)

            if data and (data.get('emails') or data.get('phones') or data.get('names')):
                collected_data.append(data)

            pages_to_visit.extend(links)
            time.sleep(self.delay)  # Respecter le délai entre les requêtes

        return collected_data

    def process_websites(self, websites):
        """Traite une liste de sites web en parallèle"""
        all_results = []

        with concurrent.futures.ThreadPoolExecutor(max_workers=self.threads) as executor:
            future_to_url = {executor.submit(self.crawl_website, url): url for url in websites}
            for future in concurrent.futures.as_completed(future_to_url):
                url = future_to_url[future]
                try:
                    results = future.result()
                    if results:
                        all_results.extend(results)
                        print(f"Terminé: {url} - {len(results)} page(s) avec des données")
                    else:
                        print(f"Aucune donnée trouvée sur: {url}")
                except Exception as e:
                    print(f"Erreur lors du traitement de {url}: {e}")

        return all_results

    def save_to_csv(self, data, output_file):
        """Sauvegarde les résultats dans un fichier CSV"""
        if not data:
            print("Aucune donnée à sauvegarder.")
            return

        try:
            with open(output_file, 'w', newline='', encoding='utf-8') as f:
                fieldnames = ['company_name', 'url', 'emails', 'phones', 'names']
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()

                for item in data:
                    writer.writerow({
                        'company_name': item.get('company_name', ''),
                        'url': item.get('url', ''),
                        'emails': '; '.join(item.get('emails', [])),
                        'phones': '; '.join(item.get('phones', [])),
                        'names': '; '.join(item.get('names', []))
                    })

            print(f"Données sauvegardées dans {output_file}")
        except Exception as e:
            print(f"Erreur lors de la sauvegarde: {e}")


# Point d'entrée du programme
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Email &amp;amp; Contact Scraper')
    parser.add_argument('-i', '--input', help='Fichier contenant la liste des sites (un par ligne)')
    parser.add_argument('-o', '--output', default='contacts_scrape_results.csv', help='Fichier de sortie CSV')
    parser.add_argument('-p', '--pages', type=int, default=10, help='Nombre maximum de pages à explorer par site')
    parser.add_argument('-d', '--depth', type=int, default=2, help='Profondeur maximale de crawling')
    parser.add_argument('-t', '--threads', type=int, default=5, help='Nombre de threads pour le traitement parallèle')
    parser.add_argument('-w', '--websites', nargs='+', help='Liste de sites à analyser')

    args = parser.parse_args()

    websites = []
    if args.input:
        with open(args.input, 'r') as f:
            websites = [line.strip() for line in f if line.strip()]
    elif args.websites:
        websites = args.websites
    else:
        websites = input("Entrez les URLs des sites à analyser (séparées par des espaces): ").split()

    if not websites:
        print("Aucun site web à analyser.")
        exit(1)

    scraper = EmailScraper(max_pages=args.pages, max_depth=args.depth, threads=args.threads)
    results = scraper.process_websites(websites)
    scraper.save_to_csv(results, args.output)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>programming</category>
      <category>python</category>
      <category>brightdatachallenge</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Email Scraping : Techniques et Considérations Éthiques avec Python</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Wed, 19 Mar 2025 12:33:42 +0000</pubDate>
      <link>https://dev.to/jmegnidro/email-scraping-techniques-et-considerations-ethiques-avec-python-48go</link>
      <guid>https://dev.to/jmegnidro/email-scraping-techniques-et-considerations-ethiques-avec-python-48go</guid>
      <description>&lt;p&gt;Les outils de scraping d'emails sont devenus un élément essentiel pour de nombreuses entreprises et professionnels du marketing. Que ce soit pour la prospection, la recherche de contacts ou la génération de leads, ces outils permettent d'automatiser la collecte d'informations de contact sur le web. Cet article vous présente les principes fondamentaux du scraping d'emails, avec un exemple pratique en Python.&lt;br&gt;
Qu'est-ce que le scraping d'emails ?&lt;br&gt;
Le scraping d'emails consiste à extraire automatiquement des adresses email et autres informations de contact (noms, numéros de téléphone, etc.) à partir de sites web. Cette technique permet de constituer rapidement des bases de données de prospects ou de contacts professionnels.&lt;br&gt;
Considérations éthiques et légales&lt;br&gt;
Avant de vous lancer dans la création ou l'utilisation d'un scraper d'emails, il est crucial de prendre en compte plusieurs aspects :&lt;/p&gt;

&lt;p&gt;Respect du RGPD : En Europe, le Règlement Général sur la Protection des Données impose des restrictions strictes sur la collecte et l'utilisation de données personnelles.&lt;br&gt;
Respect des conditions d'utilisation des sites web visités&lt;br&gt;
Respect du fichier robots.txt qui indique les zones interdites au scraping&lt;br&gt;
Limitations techniques mises en place par les sites (CAPTCHA, limite de requêtes, etc.)&lt;/p&gt;

&lt;p&gt;Exemple pratique : un scraper d'emails en Python&lt;br&gt;
Voici un exemple de code Python qui illustre les principes de base du scraping d'emails :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import requests
from bs4 import BeautifulSoup
import re
import csv
import time
import concurrent.futures
from urllib.parse import urlparse, urljoin
import argparse


class EmailScraper:
    def __init__(self, max_pages=5, max_depth=2, delay=1, threads=5):
        self.max_pages = max_pages  # Maximum de pages à explorer par domaine
        self.max_depth = max_depth  # Profondeur maximale de crawling
        self.delay = delay  # Délai entre les requêtes
        self.threads = threads  # Nombre de threads pour le traitement parallèle
        self.visited_urls = set()  # URLs déjà visitées

        # User-Agent aléatoires pour éviter les blocages
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }

        # Patterns pour la recherche
        self.email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
        self.phone_pattern = r'(?:\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}'
        self.name_pattern = r'(?:Contact|About|Team|Staff).*?(?:&amp;lt;h\d&amp;gt;)(.*?)(?:&amp;lt;/h\d&amp;gt;)'

    def is_valid_url(self, url, base_domain):
        """Vérifie si l'URL appartient au même domaine"""
        try:
            parsed_url = urlparse(url)
            parsed_base = urlparse(base_domain)
            return parsed_url.netloc == parsed_base.netloc or parsed_url.netloc == ''
        except:
            return False

    def get_base_url(self, url):
        """Extrait l'URL de base d'un site"""
        parsed = urlparse(url)
        return f"{parsed.scheme}://{parsed.netloc}"

    def normalize_url(self, url, base_url):
        """Normalise les URLs relatives"""
        if not url:
            return None
        if url.startswith(('http://', 'https://')):
            return url
        return urljoin(base_url, url)

    def extract_company_info(self, soup, url):
        """Tente d'extraire le nom de l'entreprise"""
        company_name = ""

        # Essaie de trouver le nom via le title
        if soup.title:
            title = soup.title.string
            if title:
                company_name = title.split('|')[0].split('-')[0].strip()

        # Essaie de trouver via les métadonnées
        meta_og_site_name = soup.find('meta', property='og:site_name')
        if meta_og_site_name and meta_og_site_name.get('content'):
            company_name = meta_og_site_name.get('content')

        # Si on n'a pas trouvé de nom, utiliser le domaine
        if not company_name:
            domain = urlparse(url).netloc
            company_name = domain.replace('www.', '').split('.')[0].capitalize()

        return company_name

    def extract_data_from_page(self, url, base_url, depth=0):
        """Extrait les données d'une page et retourne les liens pour crawling"""
        if url in self.visited_urls or depth &amp;gt;= self.max_depth:
            return [], {}

        self.visited_urls.add(url)
        print(f"Traitement de: {url}")

        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            if response.status_code != 200:
                return [], {}

            soup = BeautifulSoup(response.text, 'html.parser')
            page_text = soup.get_text()

            # Extraction des emails
            emails = set(re.findall(self.email_pattern, page_text))
            for link in soup.find_all('a'):
                href = link.get('href', '')
                if 'mailto:' in href:
                    email = href.split('mailto:')[1].split('?')[0]
                    emails.add(email)

            # Extraction des téléphones
            phones = set(re.findall(self.phone_pattern, page_text))

            # Extraction des noms (simple - peut être améliorée)
            names = []
            for contact_section in soup.find_all(['div', 'section'], class_=lambda c: c and (
                    'contact' in c.lower() or 'team' in c.lower())):
                person_elements = contact_section.find_all(['h2', 'h3', 'h4', 'strong'])
                for element in person_elements:
                    if element.text and len(element.text.strip()) &amp;lt; 50:  # Éviter les faux positifs
                        names.append(element.text.strip())

            # Si on est sur la page d'accueil, essayer d'extraire le nom de l'entreprise
            company_name = ""
            if depth == 0 or "about" in url.lower() or "contact" in url.lower():
                company_name = self.extract_company_info(soup, url)

            # Trouver d'autres liens à explorer
            links_to_follow = []
            if len(self.visited_urls) &amp;lt; self.max_pages:
                for link in soup.find_all('a'):
                    href = link.get('href')
                    if href:
                        normalized_url = self.normalize_url(href, base_url)
                        if normalized_url and self.is_valid_url(normalized_url,
                                                                base_url) and normalized_url not in self.visited_urls:
                            # Prioriser les pages de contact
                            if 'contact' in normalized_url.lower():
                                links_to_follow.insert(0, normalized_url)
                            else:
                                links_to_follow.append(normalized_url)

            data = {
                'url': url,
                'company_name': company_name,
                'emails': list(emails),
                'phones': list(phones),
                'names': names
            }

            return links_to_follow, data

        except Exception as e:
            print(f"Erreur sur {url}: {e}")
            return [], {}

    def crawl_website(self, start_url):
        """Fonction principale pour explorer un site web"""
        if not start_url.startswith(('http://', 'https://')):
            start_url = 'https://' + start_url

        base_url = self.get_base_url(start_url)
        pages_to_visit = [start_url]
        collected_data = []

        while pages_to_visit and len(self.visited_urls) &amp;lt; self.max_pages:
            current_url = pages_to_visit.pop(0)
            links, data = self.extract_data_from_page(current_url, base_url)

            if data and (data.get('emails') or data.get('phones') or data.get('names')):
                collected_data.append(data)

            pages_to_visit.extend(links)
            time.sleep(self.delay)  # Respecter le délai entre les requêtes

        return collected_data

    def process_websites(self, websites):
        """Traite une liste de sites web en parallèle"""
        all_results = []

        with concurrent.futures.ThreadPoolExecutor(max_workers=self.threads) as executor:
            future_to_url = {executor.submit(self.crawl_website, url): url for url in websites}
            for future in concurrent.futures.as_completed(future_to_url):
                url = future_to_url[future]
                try:
                    results = future.result()
                    if results:
                        all_results.extend(results)
                        print(f"Terminé: {url} - {len(results)} page(s) avec des données")
                    else:
                        print(f"Aucune donnée trouvée sur: {url}")
                except Exception as e:
                    print(f"Erreur lors du traitement de {url}: {e}")

        return all_results

    def save_to_csv(self, data, output_file):
        """Sauvegarde les résultats dans un fichier CSV"""
        if not data:
            print("Aucune donnée à sauvegarder.")
            return

        try:
            with open(output_file, 'w', newline='', encoding='utf-8') as f:
                fieldnames = ['company_name', 'url', 'emails', 'phones', 'names']
                writer = csv.DictWriter(f, fieldnames=fieldnames)
                writer.writeheader()

                for item in data:
                    writer.writerow({
                        'company_name': item.get('company_name', ''),
                        'url': item.get('url', ''),
                        'emails': '; '.join(item.get('emails', [])),
                        'phones': '; '.join(item.get('phones', [])),
                        'names': '; '.join(item.get('names', []))
                    })

            print(f"Données sauvegardées dans {output_file}")
        except Exception as e:
            print(f"Erreur lors de la sauvegarde: {e}")


# Point d'entrée du programme
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Email &amp;amp; Contact Scraper')
    parser.add_argument('-i', '--input', help='Fichier contenant la liste des sites (un par ligne)')
    parser.add_argument('-o', '--output', default='contacts_scrape_results.csv', help='Fichier de sortie CSV')
    parser.add_argument('-p', '--pages', type=int, default=10, help='Nombre maximum de pages à explorer par site')
    parser.add_argument('-d', '--depth', type=int, default=2, help='Profondeur maximale de crawling')
    parser.add_argument('-t', '--threads', type=int, default=5, help='Nombre de threads pour le traitement parallèle')
    parser.add_argument('-w', '--websites', nargs='+', help='Liste de sites à analyser')

    args = parser.parse_args()

    websites = []
    if args.input:
        with open(args.input, 'r') as f:
            websites = [line.strip() for line in f if line.strip()]
    elif args.websites:
        websites = args.websites
    else:
        websites = input("Entrez les URLs des sites à analyser (séparées par des espaces): ").split()

    if not websites:
        print("Aucun site web à analyser.")
        exit(1)

    scraper = EmailScraper(max_pages=args.pages, max_depth=args.depth, threads=args.threads)
    results = scraper.process_websites(websites)
    scraper.save_to_csv(results, args.output)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ce script est beaucoup plus avancé et vous permet de :&lt;/p&gt;

&lt;p&gt;Traiter plusieurs sites en masse soit via un fichier d'entrée, soit en ligne de commande&lt;br&gt;
Explorer automatiquement les sites en profondeur (crawling)&lt;br&gt;
Extraire davantage d'informations : emails, numéros de téléphone, noms potentiels, nom de l'entreprise&lt;br&gt;
Utiliser le multithreading pour accélérer le processus&lt;br&gt;
Exporter les résultats dans un fichier CSV&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Introduction à l’Automatisation des Contenus sur les Réseaux Sociaux</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Mon, 17 Mar 2025 17:55:37 +0000</pubDate>
      <link>https://dev.to/jmegnidro/introduction-a-lautomatisation-des-contenus-sur-les-reseaux-sociaux-10l</link>
      <guid>https://dev.to/jmegnidro/introduction-a-lautomatisation-des-contenus-sur-les-reseaux-sociaux-10l</guid>
      <description>&lt;p&gt;Dans un monde où les réseaux sociaux jouent un rôle central dans la communication, la gestion manuelle des publications peut rapidement devenir chronophage. Que vous soyez un créateur de contenu, une entreprise ou un marketeur, automatiser la création, la planification et la publication de contenus peut vous faire gagner du temps tout en maintenant une présence en ligne cohérente. Dans cet article, nous allons explorer les bases de l’automatisation des contenus sur les réseaux sociaux, ses avantages, et une introduction à la mise en œuvre technique avec des outils comme Python et Django REST Framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi Automatiser les Contenus ?
&lt;/h2&gt;

&lt;p&gt;L’automatisation répond à plusieurs besoins :&lt;br&gt;
Efficacité : Programmer des posts à l’avance pour éviter les publications manuelles répétitives.&lt;/p&gt;

&lt;p&gt;Cohérence : Maintenir un rythme de publication régulier, essentiel pour engager une audience.&lt;/p&gt;

&lt;p&gt;Personnalisation : Générer du contenu adapté à vos followers grâce à des données ou des algorithmes.&lt;/p&gt;

&lt;p&gt;Échelle : Gérer plusieurs comptes ou plateformes simultanément.&lt;/p&gt;

&lt;p&gt;Des outils comme Buffer ou Hootsuite offrent des solutions prêtes à l’emploi, mais créer votre propre système d’automatisation vous donne un contrôle total et une flexibilité accrue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les Étapes Clés de l’Automatisation
&lt;/h2&gt;

&lt;p&gt;Collecte ou Génération de Contenu : Rassembler des idées (via scraping, API, ou création automatique).&lt;/p&gt;

&lt;p&gt;Traitement : Formater le contenu pour chaque plateforme (Twitter : 280 caractères, Instagram : visuels, etc.).&lt;/p&gt;

&lt;p&gt;Planification : Définir quand et où publier.&lt;/p&gt;

&lt;p&gt;Publication : Utiliser des API officielles (comme Twitter API, Instagram Graph API) pour poster automatiquement.&lt;/p&gt;

&lt;p&gt;Pour illustrer, imaginons une petite application qui génère et stocke des idées de posts. Nous allons utiliser Python et Django REST Framework (DRF), un outil puissant pour créer des API.&lt;br&gt;
L’automatisation des contenus sur les réseaux sociaux est à la portée de tous avec les bons outils. Que vous utilisiez des solutions prêtes à l’emploi ou que vous codiez votre propre système, comme avec Django REST Framework, elle peut transformer votre gestion de contenu. Commencez petit — une API simple comme celle ci-dessus — et ajoutez des fonctionnalités (analyse de performance, génération IA) au fur et à mesure.&lt;/p&gt;

</description>
      <category>django</category>
      <category>linkedin</category>
      <category>python</category>
      <category>ia</category>
    </item>
    <item>
      <title>🚀 The Best 100% French Hosting Solution – Unlimited &amp; High-Performance! 🇫🇷</title>
      <dc:creator>Dominique Megnidro</dc:creator>
      <pubDate>Thu, 20 Feb 2025 16:13:21 +0000</pubDate>
      <link>https://dev.to/jmegnidro/the-best-100-french-hosting-solution-unlimited-high-performance-1mdm</link>
      <guid>https://dev.to/jmegnidro/the-best-100-french-hosting-solution-unlimited-high-performance-1mdm</guid>
      <description>&lt;p&gt;If you're looking for a &lt;strong&gt;100% French hosting provider&lt;/strong&gt; offering &lt;strong&gt;top performance&lt;/strong&gt;, &lt;strong&gt;unlimited bandwidth&lt;/strong&gt;, and &lt;strong&gt;responsive customer support&lt;/strong&gt;, then &lt;strong&gt;o2switch&lt;/strong&gt; is the perfect choice for you! 💡  &lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Unlimited hosting&lt;/strong&gt; (storage, databases, emails...)&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Powerful and optimized servers&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Free SSL certificate&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Simple and intuitive interface&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Ultra-responsive customer support&lt;/strong&gt; 🇫🇷  &lt;/p&gt;

&lt;p&gt;I host several projects with them, and I can assure you their &lt;strong&gt;value for money is unbeatable!&lt;/strong&gt; 💯  &lt;/p&gt;

&lt;p&gt;💰 &lt;strong&gt;Get started now&lt;/strong&gt; through this link: &lt;a href="https://clients.o2switch.fr/offre-hebergement-unique?sc=d05d576c83" rel="noopener noreferrer"&gt;click here&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;If you have any questions about hosting or need advice, feel free to reach out! 😊&lt;/p&gt;

</description>
      <category>django</category>
      <category>cpanel</category>
      <category>python</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
