<?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: Amine Affif</title>
    <description>The latest articles on DEV Community by Amine Affif (@amineaffif).</description>
    <link>https://dev.to/amineaffif</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4162234%2Fcb499ed5-5a3a-48e3-b1f2-7394a22ef75b.jpg</url>
      <title>DEV Community: Amine Affif</title>
      <link>https://dev.to/amineaffif</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amineaffif"/>
    <language>en</language>
    <item>
      <title>Construire son propre zoom produit : les trois détails qui comptent</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Mon, 02 Nov 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/construire-son-propre-zoom-produit-les-trois-details-qui-comptent-3dba</link>
      <guid>https://dev.to/amineaffif/construire-son-propre-zoom-produit-les-trois-details-qui-comptent-3dba</guid>
      <description>&lt;p&gt;Sur une boutique en ligne, le client ne peut pas toucher le produit. Il ne lui reste que la photo. Si elle ne lui montre pas le détail qu'il cherche, il hésite, et souvent il n'achète pas.&lt;/p&gt;

&lt;p&gt;Pour une marque D2C, j'ai développé un zoom produit sur mesure. La fonctionnalité est petite, mais elle porte directement sur la décision d'achat, et je voulais la maîtriser : son comportement, son poids, sa fluidité.&lt;/p&gt;

&lt;p&gt;Le principe, dans sa version minimale. Au survol, l'image grossit et le point de zoom suit la souris.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.zoom&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;zoom-in&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.zoom&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transform-origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt; &lt;span class="m"&gt;.2s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.zoom&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousemove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trois détails qui font la différence :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Une image plus grande que celle affichée.&lt;/strong&gt; Zoomer sur une miniature donne du flou. Il faut charger la version haute définition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pas de survol sur mobile.&lt;/strong&gt; Un toucher doit activer ou désactiver le zoom, sinon la moitié des visiteurs n'y a pas accès.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le zoom ne doit rien déplacer.&lt;/strong&gt; Si le survol décale la page, le visiteur perd sa place et quitte.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;C'est une petite fonctionnalité, mais elle se trouve sur le chemin de l'achat, et c'est pour ça que je voulais la maîtriser.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, développeur full-stack à Paris. &lt;a href="https://amineaffif.com" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>javascript</category>
      <category>css</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A 500 on every page: two dev servers, one cache folder</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Sun, 01 Nov 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/a-500-on-every-page-two-dev-servers-one-cache-folder-1mij</link>
      <guid>https://dev.to/amineaffif/a-500-on-every-page-two-dev-servers-one-cache-folder-1mij</guid>
      <description>&lt;p&gt;My Next.js dev server suddenly returned a 500 on every page, with this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Module not found: Can't resolve
'@vercel/turbopack-next/internal/font/google/font'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was about a Google font. My first thought was the network. I checked that the font servers answered. They did.&lt;/p&gt;

&lt;p&gt;What had changed: I'd run &lt;code&gt;next build&lt;/code&gt; while &lt;code&gt;next dev&lt;/code&gt; was running. Both write to the same folder, &lt;code&gt;.next&lt;/code&gt;. After alternating for a while, it held 281 MB of mixed artifacts, and Turbopack had lost its font import table in there.&lt;/p&gt;

&lt;p&gt;Starting on a fresh folder fixed everything. But purging every time isn't a solution. Better to prevent the collision: one folder per use.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;next.config.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;distDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_DIST_DIR&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;package.json&lt;/code&gt;:&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="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NEXT_DIST_DIR=.next-dev next dev --turbopack"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build keeps &lt;code&gt;.next&lt;/code&gt;, and a verification build can use a third folder: &lt;code&gt;NEXT_DIST_DIR=.next-verify next build&lt;/code&gt;. Remember to add them to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since then, the dev server and the build no longer step on each other.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, full-stack developer in Paris. &lt;a href="https://amineaffif.com/en" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>turbopack</category>
      <category>debugging</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Un 500 sur toutes les pages : deux serveurs de dev, un seul dossier de cache</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Sat, 31 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/un-500-sur-toutes-les-pages-deux-serveurs-de-dev-un-seul-dossier-de-cache-3mmj</link>
      <guid>https://dev.to/amineaffif/un-500-sur-toutes-les-pages-deux-serveurs-de-dev-un-seul-dossier-de-cache-3mmj</guid>
      <description>&lt;p&gt;Mon serveur de dev Next.js a soudain renvoyé un 500 sur toutes les pages, avec cette erreur :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Module not found: Can't resolve
'@vercel/turbopack-next/internal/font/google/font'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Elle parlait d'une police Google. J'ai d'abord pensé au réseau. J'ai vérifié que les serveurs de polices répondaient. Ils répondaient.&lt;/p&gt;

&lt;p&gt;Ce qui avait changé : j'avais lancé &lt;code&gt;next build&lt;/code&gt; pendant que &lt;code&gt;next dev&lt;/code&gt; tournait. Les deux écrivent dans le même dossier, &lt;code&gt;.next&lt;/code&gt;. À force d'alterner, il pesait 281 Mo d'artefacts mélangés, et Turbopack y avait perdu sa table d'imports de polices.&lt;/p&gt;

&lt;p&gt;Démarrer sur un dossier neuf a tout réparé. Mais purger à chaque fois n'est pas une solution. Mieux vaut empêcher la collision : un dossier par usage.&lt;/p&gt;

&lt;p&gt;Dans &lt;code&gt;next.config.mjs&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;distDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_DIST_DIR&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dans &lt;code&gt;package.json&lt;/code&gt; :&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="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NEXT_DIST_DIR=.next-dev next dev --turbopack"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le build garde &lt;code&gt;.next&lt;/code&gt;, et un build de vérification peut utiliser un troisième dossier : &lt;code&gt;NEXT_DIST_DIR=.next-verify next build&lt;/code&gt;. Pense à les ajouter au &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Depuis, le serveur de dev et le build ne se marchent plus dessus.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, développeur full-stack à Paris. &lt;a href="https://amineaffif.com" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>turbopack</category>
      <category>debugging</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Un seul jeu de données pour mon CV et mon site : fini les dates qui ne concordent pas</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/un-seul-jeu-de-donnees-pour-mon-cv-et-mon-site-fini-les-dates-qui-ne-concordent-pas-5cbf</link>
      <guid>https://dev.to/amineaffif/un-seul-jeu-de-donnees-pour-mon-cv-et-mon-site-fini-les-dates-qui-ne-concordent-pas-5cbf</guid>
      <description>&lt;p&gt;Un lecteur sérieux ouvre ton CV et ton site côte à côte. Si une date ou un intitulé ne concorde pas, c'est la première chose qu'il remarque.&lt;/p&gt;

&lt;p&gt;Mon CV en PDF et mon site viennent d'un seul jeu de données : des fichiers TypeScript qui décrivent mes expériences et mes projets. Le site les affiche en React. Le CV est produit par un script qui lit les mêmes fichiers.&lt;/p&gt;

&lt;p&gt;Le principe, simplifié :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// content/experience.ts : la seule source&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;experience&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;company&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delivered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/build-cv.mjs : lit le même fichier, produit le HTML, puis le PDF&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;experience&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content/experience.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;experience&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux choix qui comptent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lire les données plutôt que compiler le TypeScript.&lt;/strong&gt; Le script extrait le tableau par lecture. Ça évite de monter une chaîne de build juste pour fabriquer un PDF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imprimer depuis Chrome en ligne de commande.&lt;/strong&gt; Le CV est un HTML mis en page pour une page A4, converti avec &lt;code&gt;--print-to-pdf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Au quotidien : je corrige une date à un seul endroit, je régénère, et le site comme le PDF sont à jour. Plus de version oubliée dans un coin.&lt;/p&gt;

&lt;p&gt;Ça vaut pour un CV, mais l'idée vaut pour tout document qui répète une information : une seule source, plusieurs sorties.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, développeur full-stack à Paris. &lt;a href="https://amineaffif.com" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>typescript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Une landing page pour investisseurs en quelques jours, à partir de photos de téléphone</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/une-landing-page-pour-investisseurs-en-quelques-jours-a-partir-de-photos-de-telephone-3cjg</link>
      <guid>https://dev.to/amineaffif/une-landing-page-pour-investisseurs-en-quelques-jours-a-partir-de-photos-de-telephone-3cjg</guid>
      <description>&lt;p&gt;Un fondateur avait quelques jours pour présenter son projet à des investisseurs. Un projet ambitieux, mais pour toute matière : quelques photos prises au téléphone.&lt;/p&gt;

&lt;p&gt;La contrainte n'était pas la qualité des photos, c'était la date. Attendre un vrai shooting n'était pas une option. J'ai décidé de partir de ce qu'il avait.&lt;/p&gt;

&lt;p&gt;J'ai tout pris en charge, seul : le design, le texte, le développement en Next.js. À partir des photos brutes, j'ai retravaillé les visuels, ajouté des rendus 3D et du motion. Puis une landing page complète qui déroule le projet section après section.&lt;/p&gt;

&lt;p&gt;Pourquoi une seule personne ? Sur un délai de quelques jours, chaque passage de relais ralentit. Quand le design, le texte et le code sont tenus par la même personne, les décisions se prennent en une phrase, pas en réunion.&lt;/p&gt;

&lt;p&gt;La page était prête pour la présentation. Le fondateur a levé les fonds ; la page faisait partie de son dossier.&lt;/p&gt;

&lt;p&gt;Sur une page qui doit convaincre, le texte et le visuel pèsent autant que le code. Je les ai traités de la même façon : une section à la fois, chacune avec un seul message.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, développeur full-stack à Paris. &lt;a href="https://amineaffif.com" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>design</category>
      <category>startup</category>
    </item>
    <item>
      <title>Why your PDF's text comes out as separate letters</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Sat, 24 Oct 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/why-your-pdfs-text-comes-out-as-separate-letters-1hdj</link>
      <guid>https://dev.to/amineaffif/why-your-pdfs-text-comes-out-as-separate-letters-1hdj</guid>
      <description>&lt;p&gt;I generated a PDF from HTML with Chrome on the command line. To the eye, it was perfect. But when I extracted its text with a pdf.js-based tool, my section headings came out like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;E X P É R I E N C E
STA C K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;pdftotext&lt;/code&gt; (poppler), the same words came out whole. Same file, two readings.&lt;/p&gt;

&lt;p&gt;I looked inside the PDF. In mine, Chrome doesn't write words but roughly one character per operation: 3,136 text-drawing operations for 3,062 repositioning ones. Every letter is placed absolutely.&lt;/p&gt;

&lt;p&gt;My headings had &lt;code&gt;letter-spacing: .13em&lt;/code&gt;. Removing the letter spacing made the words come out whole in both tools. I make up for it with font size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* before */&lt;/span&gt;
&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;letter-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.13em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8.4pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* after */&lt;/span&gt;
&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;letter-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A PDF isn't text, it's a list of positioned glyphs. That it looks right to the eye doesn't guarantee a machine can read it.&lt;/p&gt;

&lt;p&gt;If your PDF's text needs to be found, copied or indexed, test it with at least two extractors. One alone can tell you everything is fine.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, full-stack developer in Paris. &lt;a href="https://amineaffif.com/en" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>webdev</category>
      <category>debugging</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Pourquoi le texte de ton PDF ressort en lettres séparées</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Fri, 23 Oct 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/pourquoi-le-texte-de-ton-pdf-ressort-en-lettres-separees-1j4g</link>
      <guid>https://dev.to/amineaffif/pourquoi-le-texte-de-ton-pdf-ressort-en-lettres-separees-1j4g</guid>
      <description>&lt;p&gt;J'ai généré un PDF depuis du HTML avec Chrome en ligne de commande. À l'œil, il était parfait. Mais quand j'en ai extrait le texte avec un outil basé sur pdf.js, mes titres de section sortaient comme ça :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;E X P É R I E N C E
STA C K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avec &lt;code&gt;pdftotext&lt;/code&gt; (poppler), les mêmes mots sortaient entiers. Même fichier, deux lectures.&lt;/p&gt;

&lt;p&gt;J'ai regardé l'intérieur du PDF. Dans le mien, Chrome n'écrit pas des mots mais à peu près un caractère par opération : 3 136 opérations d'affichage de texte pour 3 062 repositionnements. Chaque lettre est placée en absolu.&lt;/p&gt;

&lt;p&gt;Mes titres avaient un &lt;code&gt;letter-spacing: .13em&lt;/code&gt;. En retirant l'interlettrage, les mots sont ressortis entiers dans les deux outils. Je compense en taille de police :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* avant */&lt;/span&gt;
&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;letter-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.13em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8.4pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* après */&lt;/span&gt;
&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;letter-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un PDF n'est pas du texte, c'est une liste de glyphes positionnés. Qu'il soit beau à l'œil ne garantit pas qu'une machine saura le lire.&lt;/p&gt;

&lt;p&gt;Si le texte de ton PDF doit être trouvé, copié ou indexé, teste-le avec au moins deux extracteurs. Un seul peut te dire que tout va bien.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, développeur full-stack à Paris. &lt;a href="https://amineaffif.com" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pdf</category>
      <category>webdev</category>
      <category>debugging</category>
      <category>javascript</category>
    </item>
    <item>
      <title>J'ai relu deux fois plus de code que je n'en ai écrit : ce que ça m'a appris</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Wed, 21 Oct 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/jai-relu-deux-fois-plus-de-code-que-je-nen-ai-ecrit-ce-que-ca-ma-appris-14ff</link>
      <guid>https://dev.to/amineaffif/jai-relu-deux-fois-plus-de-code-que-je-nen-ai-ecrit-ce-que-ca-ma-appris-14ff</guid>
      <description>&lt;p&gt;Avec les agents de code, on produit plus vite qu'on ne relit. Le goulot, c'est maintenant la relecture.&lt;/p&gt;

&lt;p&gt;Sur quinze mois, j'ai relu à peu près deux fois plus de pull requests que je n'en ai écrites, sur huit dépôts et trois stacks. Je le vois comme du temps donné à l'équipe : une pull request qui n'est pas relue, c'est du code que personne n'a lu qui part en production.&lt;/p&gt;

&lt;p&gt;Trois habitudes qui rendent la relecture possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Livrer petit.&lt;/strong&gt; Mes pull requests font deux à cinq fichiers en médiane. Une PR qui touche trois sujets ne se relit pas, elle s'approuve par fatigue. J'en ouvre trois.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Découper un gros chantier en étapes lisibles.&lt;/strong&gt; Le helper, puis l'interface, puis le branchement aux données, puis les tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Écrire le pourquoi, pas le quoi.&lt;/strong&gt; La description dit ce que ça change et pourquoi. Le diff montre déjà le reste. S'il y a une décision discutable, je l'écris dans la PR plutôt que d'attendre qu'on la trouve.&lt;/p&gt;

&lt;p&gt;Ce que j'attends d'une revue : qu'on discute les décisions, pas la syntaxe. Le formatage est un travail d'outil, pas de relecteur.&lt;/p&gt;

&lt;p&gt;Relire demande de comprendre le code de l'autre, pas seulement de vérifier qu'il compile. C'est aussi un bon moyen d'apprendre une base de code qu'on ne connaît pas encore.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, développeur full-stack à Paris. &lt;a href="https://amineaffif.com" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codereview</category>
      <category>git</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>Deposits and holds with Stripe: what actually happens</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Mon, 19 Oct 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/deposits-and-holds-with-stripe-what-actually-happens-43g6</link>
      <guid>https://dev.to/amineaffif/deposits-and-holds-with-stripe-what-actually-happens-43g6</guid>
      <description>&lt;p&gt;On a vehicle rental platform, I had to handle deposits and security holds with Stripe. They both look like a payment, but they're not the same operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A deposit&lt;/strong&gt; is a normal payment: the money is taken right away, for part of the price.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A security hold&lt;/strong&gt; must not be collected. You block it on the customer's card, then release it when the vehicle comes back in good shape. In Stripe, that's separating authorization from capture:&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;# example: €500 held, nothing collected&lt;/span&gt;
curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="s2"&gt;:"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eur &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"automatic_payment_methods[enabled]=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;capture_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;manual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the card is confirmed, the payment moves to &lt;code&gt;requires_capture&lt;/code&gt;. The money is held, not collected.&lt;/p&gt;

&lt;p&gt;Three things to know.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The authorization expires.&lt;/strong&gt; For a card paid online, it's usually 7 days. After that, the hold is released and the payment moves to &lt;code&gt;canceled&lt;/code&gt;. For a longer rental, a hold placed at pickup doesn't last: you need an extended authorization (only for certain cases), or save the payment method to charge later if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can capture less than the held amount.&lt;/strong&gt; With &lt;code&gt;amount_to_capture&lt;/code&gt;, the rest is released automatically. But in most cases, you can only capture once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releasing means canceling the payment.&lt;/strong&gt; Nothing more.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One detail on the customer side: some bank statements don't distinguish a held amount from a charged one. Better to say so in the confirmation email.&lt;/p&gt;

&lt;p&gt;And not every payment method allows it: SEPA direct debit or iDEAL, for example, don't support this separation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, full-stack developer in Paris. &lt;a href="https://amineaffif.com/en" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>payments</category>
      <category>webdev</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Acomptes et cautions avec Stripe : ce qui se passe vraiment</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Sun, 18 Oct 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/acomptes-et-cautions-avec-stripe-ce-qui-se-passe-vraiment-2ib7</link>
      <guid>https://dev.to/amineaffif/acomptes-et-cautions-avec-stripe-ce-qui-se-passe-vraiment-2ib7</guid>
      <description>&lt;p&gt;Sur une plateforme de location de véhicules, j'ai eu à gérer des acomptes et des cautions avec Stripe. Les deux ressemblent à un paiement, mais ce n'est pas la même opération.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L'acompte&lt;/strong&gt; est un paiement normal : l'argent est pris tout de suite, pour une partie du prix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La caution&lt;/strong&gt; ne doit pas être encaissée. On la bloque sur la carte du client, puis on la libère quand le véhicule revient en bon état. Chez Stripe, ça s'appelle séparer l'autorisation de la capture :&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;# exemple : 500 € bloqués, rien d'encaissé&lt;/span&gt;
curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="s2"&gt;:"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eur &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"automatic_payment_methods[enabled]=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;capture_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;manual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Une fois la carte confirmée, le paiement passe en &lt;code&gt;requires_capture&lt;/code&gt;. L'argent est retenu, pas encaissé.&lt;/p&gt;

&lt;p&gt;Trois choses à savoir.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;L'autorisation expire.&lt;/strong&gt; Pour une carte payée en ligne, c'est en général 7 jours. Passé ce délai, le blocage est levé et le paiement passe en &lt;code&gt;canceled&lt;/code&gt;. Pour une location plus longue, la caution bloquée au départ ne tient plus : il faut une autorisation prolongée (réservée à certains cas), ou enregistrer le moyen de paiement pour débiter plus tard si besoin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On peut capturer moins que le montant bloqué.&lt;/strong&gt; Avec &lt;code&gt;amount_to_capture&lt;/code&gt;, le reste est libéré automatiquement. Mais dans la plupart des cas, on ne peut capturer qu'une seule fois.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Libérer, c'est annuler le paiement.&lt;/strong&gt; Rien de plus.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Un détail côté client : certains relevés bancaires ne font pas la différence entre une somme bloquée et une somme prélevée. Mieux vaut le dire dans l'email de confirmation.&lt;/p&gt;

&lt;p&gt;Et tous les moyens de paiement ne le permettent pas : le prélèvement SEPA ou iDEAL, par exemple, ne supportent pas cette séparation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, développeur full-stack à Paris. &lt;a href="https://amineaffif.com" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>payments</category>
      <category>webdev</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Giving tools to an AI agent: why I set the limit outside the prompt</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Sat, 17 Oct 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/giving-tools-to-an-ai-agent-why-i-set-the-limit-outside-the-prompt-586j</link>
      <guid>https://dev.to/amineaffif/giving-tools-to-an-ai-agent-why-i-set-the-limit-outside-the-prompt-586j</guid>
      <description>&lt;p&gt;When I give tools to an agent, I treat it like a third-party dependency: I don't trust it, I contain it.&lt;/p&gt;

&lt;p&gt;The rule that matters: the constraint is set &lt;strong&gt;when the process starts&lt;/strong&gt;, not in the prompt. A prompt is a request. The model can ignore it, or be turned around by malicious text slipped into what it reads.&lt;/p&gt;

&lt;p&gt;On an agent I built, the tool list is closed in advance: twelve read tools allowed, fifteen write tools refused. It can't write a file or send a message, even if it tries.&lt;/p&gt;

&lt;p&gt;Three more protections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an anti-injection section in the prompt, because an agent reads text it doesn't control,&lt;/li&gt;
&lt;li&gt;a mandatory adversarial pass before every output,&lt;/li&gt;
&lt;li&gt;human validation before anything is published.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why this instead of "a good prompt": an agent that can act does what its environment allows, not what it was politely asked. If the action is impossible, there's nothing left to trust.&lt;/p&gt;

&lt;p&gt;I care less about the speed these tools bring than about how to contain them: that's what lets you move fast without pulling code into a repo that nobody has read.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, full-stack developer in Paris. &lt;a href="https://amineaffif.com/en" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Donner des outils à un agent IA : pourquoi je pose la limite hors du prompt</title>
      <dc:creator>Amine Affif</dc:creator>
      <pubDate>Fri, 16 Oct 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/amineaffif/donner-des-outils-a-un-agent-ia-pourquoi-je-pose-la-limite-hors-du-prompt-3e29</link>
      <guid>https://dev.to/amineaffif/donner-des-outils-a-un-agent-ia-pourquoi-je-pose-la-limite-hors-du-prompt-3e29</guid>
      <description>&lt;p&gt;Quand je donne des outils à un agent, je le traite comme une dépendance tierce : je ne lui fais pas confiance, je l'encadre.&lt;/p&gt;

&lt;p&gt;La règle qui compte : la contrainte se pose &lt;strong&gt;au lancement du processus&lt;/strong&gt;, pas dans le prompt. Un prompt, c'est une demande. Le modèle peut l'ignorer, ou se faire retourner par un texte malveillant glissé dans ce qu'il lit.&lt;/p&gt;

&lt;p&gt;Sur un agent que j'ai construit, la liste des outils est fermée à l'avance : douze outils de lecture autorisés, quinze outils d'écriture refusés. Il ne peut ni écrire un fichier ni envoyer un message, même s'il essaie.&lt;/p&gt;

&lt;p&gt;Trois protections s'ajoutent :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;une section anti-injection dans le prompt, parce qu'un agent lit des textes qu'il ne maîtrise pas,&lt;/li&gt;
&lt;li&gt;une relecture contradictoire obligatoire avant chaque sortie,&lt;/li&gt;
&lt;li&gt;une validation humaine avant toute publication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pourquoi ce choix plutôt que « un bon prompt » : un agent qui peut agir fait ce que son environnement lui permet, pas ce qu'on lui a gentiment demandé. Si l'action est impossible, il n'y a plus à lui faire confiance.&lt;/p&gt;

&lt;p&gt;Je m'intéresse moins à la vitesse que ces outils apportent qu'à la façon de les contenir : c'est ce qui permet d'aller vite sans ramener dans un dépôt du code que personne n'a lu.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amine Affif, développeur full-stack à Paris. &lt;a href="https://amineaffif.com" rel="noopener noreferrer"&gt;amineaffif.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
