<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" 
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Captain Ruby - Le Blog Pour Les Passionnés de Ruby</title>
    <description>Captain Ruby propose des tutoriels et des articles approfondis sur Ruby et Ruby on Rails pour les développeurs.</description>
    <link>https://www.captainruby.fr</link>
    <language>fr-FR</language>
    <managingEditor>contact@captainruby.fr (Captain Ruby Team)</managingEditor>
    <webMaster>contact@captainruby.fr (Captain Ruby Team)</webMaster>
    <lastBuildDate>Wed, 11 Mar 2026 08:00:00 +0100</lastBuildDate>
    <pubDate>Wed, 11 Mar 2026 08:00:00 +0100</pubDate>
    <generator>Captain Ruby Blog - Rails 7.1.5.2</generator>
    <docs>https://blogs.law.harvard.edu/tech/rss</docs>
    <ttl>60</ttl>
    <image>
      <url>https://www.captainruby.fr/assets/favicon_captain_ruby2-8178f5b662a291b2cc790c5899fe40dd11440720340703272c74997a286385d5.png</url>
      <title>Captain Ruby - Le Blog Pour Les Passionnés de Ruby</title>
      <link>https://www.captainruby.fr</link>
      <width>32</width>
      <height>32</height>
    </image>
    
    <!-- Auto-discovery pour validation RSS -->
    <atom:link href="https://www.captainruby.fr/rss" rel="self" type="application/rss+xml" />

      <item>
        <title><![CDATA[Envoyer des SMS avec Ruby on Rails pour la double authentification]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[
  Implémenter une 2FA par SMS avec Ruby on Rails

La double authentification par SMS, ce n&#39;est pas forcément la feature la plus excitante à coder....]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <div class="trix-content">
  <h1>Implémenter une 2FA par SMS avec Ruby on Rails</h1>
<div>La double authentification par SMS, ce n'est pas forcément la feature la plus excitante à coder. Pourtant, dès qu'un projet Rails arrive en production, c'est souvent l'une des premières briques de sécurité qu'on ajoute.</div>
<div>Un simple mot de passe ne suffit plus vraiment aujourd'hui. Fuites de données, phishing, mots de passe recyclés... les risques sont partout. Ajouter une deuxième étape de validation devient vite indispensable.</div>
<div>Dans cet article, on va voir comment mettre en place une <strong>2FA par SMS avec Ruby et Rails</strong>, de manière propre et maintenable.</div>
<div>On va couvrir deux cas très courants :</div>
<ul>
<li>une webapp Rails classique (sessions + cookies)</li>
<li>un backend Rails API consommé par une application mobile Flutter</li>
</ul>
<div>L'objectif n'est pas de vendre un provider SMS, mais plutôt de comprendre <strong>comment structurer une implémentation solide</strong> côté Rails.</div>
<h1>Pourquoi utiliser le SMS pour la 2FA</h1>
<div>Soyons honnêtes : le SMS n'est pas la méthode de 2FA la plus sécurisée du monde. Les attaques par SIM swap existent et certains services sensibles préfèrent d'autres approches.</div>
<div>Mais dans la pratique, pour beaucoup de produits web, le SMS reste un excellent compromis.</div>
<div>Pourquoi ?</div>
<ul>
<li>c'est simple pour l'utilisateur</li>
<li>aucune application supplémentaire à installer</li>
<li>mise en place rapide</li>
<li>amélioration immédiate de la sécurité</li>
</ul>
<div>Pour beaucoup de projets Rails, surtout en B2C, c'est souvent <strong>la première marche vers une authentification plus robuste</strong>.</div>
<h1>Architecture globale d'une 2FA</h1>
<div>Que tu développes une webapp ou une API mobile, le principe reste exactement le même.</div>
<ol>
<li>l'utilisateur se connecte avec son login et son mot de passe</li>
<li>le backend génère un code temporaire</li>
<li>ce code est envoyé par SMS</li>
<li>l'utilisateur saisit ce code</li>
<li>le backend vérifie la validité du code</li>
</ol>
<div>La différence se situe ensuite dans la validation finale :</div>
<ul>
<li>une <strong>session Rails</strong> dans une webapp</li>
<li>un <strong>token (JWT ou autre)</strong> dans une API</li>
</ul>
<div>Le coeur du système reste identique.</div>
<h1>Choisir un provider SMS</h1>
<div>La plupart des providers offrent aujourd'hui des API HTTP très simples à utiliser.</div>
<div>Ce qu'on attend surtout :</div>
<ul>
<li>une API fiable</li>
<li>une bonne délivrabilité</li>
<li>une facturation claire</li>
</ul>
<div>Les providers les plus utilisés avec Ruby :</div>
<ul>
<li>Twilio</li>
<li>OVH SMS</li>
<li>Vonage (anciennement Nexmo)</li>
</ul>
<div>Dans cet article on prendra <strong>Twilio</strong> comme exemple, mais l'architecture fonctionne avec n'importe quel service.</div>
<h1>Isoler l'envoi de SMS dans un service</h1>
<div>Une règle simple : <strong>ne jamais appeler directement un provider SMS depuis un controller</strong>.</div>
<div>Créer un service dédié permet :</div>
<ul>
<li>de simplifier les tests</li>
<li>de remplacer facilement le provider</li>
<li>de garder des controllers lisibles</li>
</ul>
<div>Exemple :</div>
<pre># app/services/sms_sender.rb
class SmsSender
  def self.send(to:, message:)
    client = Twilio::REST::Client.new(
      ENV["TWILIO_ACCOUNT_SID"],
      ENV["TWILIO_AUTH_TOKEN"]
    )

    client.messages.create(
      from: ENV["TWILIO_PHONE_NUMBER"],
      to: to,
      body: message
    )
  end
end
<br></pre>
<div>Ce service ne contient volontairement <strong>aucune logique métier</strong>.</div>
<div>Son rôle est simple : envoyer un SMS.</div>
<h1>Générer et stocker un code de vérification</h1>
<div>Le coeur de la 2FA est le code temporaire.</div>
<div>Une implémentation simple fonctionne très bien dans la majorité des cas :</div>
<ul>
<li>un code numérique court</li>
<li>une durée de validité limitée</li>
<li>un seul code actif</li>
</ul>
<div>Modèle ActiveRecord</div>
<pre># app/models/two_factor_code.rb
class TwoFactorCode &lt; ApplicationRecord
  belongs_to :user

  before_create :generate_code

  def expired?
    created_at &lt; 10.minutes.ago
  end

  private

  def generate_code
    self.code = rand(100_000..999_999)
  end
end
<br></pre>
<div>Quelques bonnes pratiques :</div>
<ul>
<li>éviter de stocker ces codes trop longtemps</li>
<li>ne jamais logger leur valeur</li>
<li>éventuellement hasher le code si le niveau de sécurité l'exige</li>
</ul>
<h1>Envoyer le code par SMS</h1>
<div>Quand la 2FA est déclenchée, on crée un code et on l'envoie.</div>
<pre>code = user.two_factor_codes.create!

SmsSender.send(
  to: user.phone_number,
  message: "Votre code de verification est #{code.code}"
)
<br></pre>
<div>Dans une vraie application, cet envoi doit passer par un <strong>ActiveJob</strong> pour ne pas bloquer la requête HTTP.</div>
<h1>Cas 1 : Webapp Rails classique</h1>
<div>Dans une application Rails traditionnelle, le flow ressemble à ça :</div>
<ol>
<li>login + mot de passe valide</li>
<li>génération du code</li>
<li>envoi du SMS</li>
<li>redirection vers l'écran 2FA</li>
<li>validation du code</li>
<li>activation complète de la session</li>
</ol>
<div>Controller de verification</div>
<pre>class TwoFactorController &lt; ApplicationController
  def create
    record = current_user.two_factor_codes.last

    if record&amp;.code == params[:code].to_i &amp;&amp; !record.expired?
      session[:two_factor_passed] = true
      redirect_to dashboard_path
    else
      flash[:alert] = "Code invalide ou expire"
      render :new
    end
  end
end
<br></pre>
<div>Tant que two_factor_passed n'est pas présent dans la session, l'utilisateur n'a pas accès aux pages sensibles.</div>
<h1>Cas 2 : Backend Rails API pour mobile</h1>
<div>Pour une API consommée par Flutter ou une autre app mobile, la logique est la même mais l'interface change.</div>
<div>Versionner les endpoints</div>
<div>Une API mobile peut rester longtemps sans mise à jour.</div>
<div>Le versionnement est donc essentiel.</div>
<pre>POST /api/v1/auth/2fa/send
POST /api/v1/auth/2fa/verify
<br></pre>
<div>Routes</div>
<pre>namespace :api do
  namespace :v1 do
    namespace :auth do
      post "2fa/send",   to: "two_factor#send_code"
      post "2fa/verify", to: "two_factor#verify"
    end
  end
end
<br></pre>
<div>Controller API</div>
<pre>class Api::V1::Auth::TwoFactorController &lt; Api::V1::BaseController
  def send_code
    code = current_user.two_factor_codes.create!

    SmsSender.send(
      to: current_user.phone_number,
      message: "Votre code est #{code.code}"
    )

    render json: { success: true }
  end

  def verify
    record = current_user.two_factor_codes.last

    if record&amp;.code == params[:code].to_i &amp;&amp; !record.expired?
      token = JwtEncoder.encode(user_id: current_user.id)
      render json: { success: true, token: token }
    else
      render json: { success: false }, status: :unauthorized
    end
  end
end
<br></pre>
<div>L'application Flutter se contente d'appeler ces endpoints.</div>
<div>Toute la logique de sécurité reste côté backend.</div>
<h1>Points importants a ne pas oublier</h1>
<div>Rate limiting</div>
<div>Sans limite, un utilisateur pourrait demander des dizaines de SMS en quelques minutes.</div>
<div>Il faut donc ajouter :</div>
<ul>
<li>un delai minimum entre deux envois</li>
<li>un nombre maximum de codes par jour</li>
</ul>
<div>Envoi asynchrone avec ActiveJob</div>
<div>Ne jamais envoyer un SMS dans une requête synchrone.</div>
<div>Créer un job dédié :</div>
<pre>class SendTwoFactorSmsJob &lt; ApplicationJob
  queue_as :default

  def perform(user_id, code)
    user = User.find(user_id)

    SmsSender.send(
      to: user.phone_number,
      message: "Votre code est #{code}"
    )
  end
end
<br></pre>
<div>Puis :</div>
<pre>SendTwoFactorSmsJob.perform_later(user.id, code.code)
<br></pre>
<div>Nettoyer les anciens codes</div>
<div>Un job périodique peut supprimer les codes expirés afin d'éviter d'accumuler des données inutiles.</div>
<div>Ne jamais logger les codes</div>
<div>Cela peut sembler évident, mais c'est une erreur fréquente.</div>
<div>Les codes 2FA ne doivent <strong>jamais apparaître dans les logs</strong>.</div>
<h1>Aller plus loin</h1>
<div>Une architecture propre permet ensuite d'ajouter facilement d'autres méthodes de vérification :</div>
<ul>
<li>TOTP (Google Authenticator)</li>
<li>email fallback</li>
<li>passkeys</li>
<li>WebAuthn</li>
</ul>
<div>L'idée est de garder un système évolutif.</div>
<h1>Conclusion</h1>
<div>La 2FA par SMS n'est pas parfaite, mais elle reste une excellente première couche de sécurité.</div>
<div>Avec Ruby on Rails, on dispose de tous les outils nécessaires pour construire une implémentation propre :</div>
<ul>
<li>services clairs</li>
<li>jobs asynchrones</li>
<li>API versionnée</li>
</ul>
<div>Que tu travailles sur une webapp Rails ou une API pour mobile, les principes restent exactement les mêmes.</div>
<div>Et si tu prends le temps de bien penser ton système de 2FA aujourd'hui, tu t'éviteras beaucoup de problèmes demain.</div>
<div>
<br>Happy conding!<br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi80OT9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--3917b502c4270d412c359f6dd51732975c786843" content-type="image/gif" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDksInB1ciI6ImJsb2JfaWQifX0=--9b40dc93daf75b32760d44d9fe5e6742a0b874a9/Tom%20Hanks%20Hello%20GIF.gif" filename="Tom Hanks Hello GIF.gif" filesize="1387162" width="400" height="200" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--gif">
    
    <img class="attachment--preview" src="https://res-5.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/7eqzyspv5rudl896efj8dhanwh8q">

</figure></action-text-attachment>
</div>
</div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/envoyer-des-sms-avec-ruby-on-rails-pour-la-double-authentification</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/envoyer-des-sms-avec-ruby-on-rails-pour-la-double-authentification</guid>
        <pubDate>Wed, 11 Mar 2026 08:00:00 +0100</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[two-factor-authentication]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[SMS]]></category>
            <category><![CDATA[2fa]]></category>
            <category><![CDATA[authentication]]></category>
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Intermédiaire à Confirmé]]></category>
        
        <!-- Image principale -->
          <enclosure url="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDcsInB1ciI6ImJsb2JfaWQifX0=--1fc8df6e2489b6d4fd096170946c2d54791957ef/pexels-tim-samuel-5838215.jpg" 
                     type="image/jpeg" 
                     length="1311520" />
        
        <!-- Métadonnées supplémentaires -->
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[6 min de lecture]]></category>
      </item>
      <item>
        <title><![CDATA[Générer du HTML avec Ruby (sans Rails)]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[
  Intro

Aujourd&#39;hui, on te montre comment on passe d&#39;un langage interprété (Ruby) à du HTML côté client en passant par un serveur web minimal. On...]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <div class="trix-content">
  <h1>Intro</h1>
<div>Aujourd'hui, on te montre comment on passe d'un langage interprété (Ruby) à du <strong>HTML côté client</strong> en passant par un <strong>serveur web</strong> minimal. On va rester en Ruby “pur” et bâtir petit à petit, jusqu'à un mini-projet façon <strong>mini‑rails</strong>.</div>
<div>Objectifs:</div>
<ul>
<li>utiliser <strong>Rack + Puma</strong> dans tous les exemples</li>
<li>générer du HTML de 4 façons: <strong>string</strong>, <strong>ERB</strong>, <strong>Nokogiri::Builder</strong>, <strong>Phlex</strong>
</li>
<li>expliquer les briques: <strong>Proc Rack</strong>, <strong>env</strong>, <strong>[status, headers, body]</strong>, <strong>200</strong>, <strong>Content‑Type</strong>, <strong>objet ERB</strong>
</li>
<li>pousser l'exemple <strong>Phlex</strong> avec assets (JS/CSS), <strong>routing</strong> simple, et <strong>base de données SQLite</strong> via Active Record</li>
</ul>
<div>Prérequis rapides</div>
<div>Au choix:</div>
<div><strong>Option A - gems globales</strong></div>
<pre>gem install rack puma nokogiri phlex activerecord sqlite3
<br></pre>
<div><strong>Option B - Bundler (recommandé)</strong></div>
<pre>bundle init
# Gemfile :
# gem "rack"
# gem "puma"
# gem "nokogiri"
# gem "phlex"
# gem "activerecord"
# gem "sqlite3"
bundle install
<br></pre>
<div>Dans chacun de nos scripts, on utilisera Puma: require "rack/handler/puma" puis Rack::Handler::Puma.run app, Port: 9292. Tu pourras ensuite visiter http://localhost:9292.</div>
<h1>1) HTML en string avec Rack et Puma</h1>
<div><strong>On créer le fichier app.rb</strong></div>
<pre>require "rack"
require "rack/handler/puma"

app = Proc.new do |env|
  html = &lt;&lt;~HTML
    &lt;!doctype html&gt;
    &lt;html&gt;
      &lt;head&gt;&lt;meta charset="utf-8"&gt;&lt;title&gt;Hello monde!&lt;/title&gt;&lt;/head&gt;
      &lt;body&gt;
        &lt;h1&gt;Super Ruby&lt;/h1&gt;
        &lt;p&gt;Du HTML juste avec du Ruby, vraiment ?&lt;/p&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  HTML

  [200, { "Content-Type" =&gt; "text/html; charset=utf-8" }, [html]]
end

Rack::Handler::Puma.run app, Port: 9292
<br></pre>
<div>Lance ruby app.rb, puis visite http://localhost:9292.</div>
<div>Comprendre les briques</div>
<ul>
<li>
<strong>app = Proc.new { |env| ... }</strong>: une appli Rack est <em>un objet appelable</em> qui reçoit <strong>env</strong> (un Hash avec la requête: méthode, chemin, headers...) et retourne un <strong>triplet</strong> [status, headers, body].</li>
<li>
<strong>env</strong>: exemples utiles env["REQUEST_METHOD"], env["PATH_INFO"], env["QUERY_STRING"].</li>
<li>
<strong>200</strong>: c'est le code HTTP <em>OK. </em>Comme par exemple (404: not found, 302: redirect, 500: server error).</li>
<li>
<strong>{"Content-Type" =&gt; "text/html; charset=utf-8"}</strong>: indique le type de contenu au navigateur. Pour du JSON: application/json.</li>
<li>
<strong>body</strong>: doit être énumérable (Array de strings, généralement). Ici [[html]].</li>
</ul>
<blockquote>Résumé: on construit le HTML, puis on renvoie [status, headers, body] dans cet ordre.</blockquote>
<h1>2) ERB: un template réutilisable</h1>
<div><strong>On créer le fichier index.erb</strong></div>
<pre>&lt;!doctype html&gt;
&lt;html&gt;
  &lt;head&gt;&lt;meta charset="utf-8"&gt;&lt;title&gt;&lt;%= title %&gt;&lt;/title&gt;&lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;&lt;%= title %&gt;&lt;/h1&gt;
    &lt;ul&gt;
      &lt;% items.each do |item| %&gt;
        &lt;li&gt;&lt;%= item %&gt;&lt;/li&gt;
      &lt;% end %&gt;
    &lt;/ul&gt;
  &lt;/body&gt;
&lt;/html&gt;
<br></pre>
<div><strong>Puis app_erb.rb</strong></div>
<pre>require "rack"
require "erb"
require "rack/handler/puma"

TEMPLATE = ERB.new(File.read("index.erb"))

app = Proc.new do |env|
  title = "Ma page ERB"
  items = ["Ruby", "Toulouse", "Rack", "ERB", "Captain Ruby"]

  html = TEMPLATE.result_with_hash(title: title, items: items)
  [200, { "Content-Type" =&gt; "text/html; charset=utf-8" }, [html]]
end

Rack::Handler::Puma.run app, Port: 9292
<br></pre>
<div><strong>C'est quoi un objet ERB ?</strong></div>
<ul>
<li>ERB lit un fichier HTML avec &lt;% %&gt; (Ruby) et &lt;%= %&gt; (Ruby qui affiche).</li>
<li>ERB.new(...) compile le template.</li>
<li>result_with_hash(...) évalue le template avec des variables.</li>
</ul>
<blockquote>Pour comparer les moteurs, on a un billet: <em>HAML vs ERB : quel moteur de template choisir pour vos vues Rails ?</em> sur Captain Ruby.</blockquote>
<h1>3) Nokogiri::HTML::Builder: générer le HTML en Ruby</h1>
<div><strong>A la racine: app_nokogiri.rb</strong></div>
<pre>require "rack"
require "nokogiri"
require "rack/handler/puma"

app = Proc.new do |env|
  builder = Nokogiri::HTML::Builder.new do |doc|
    doc.html do
      doc.head do
        doc.meta charset: "utf-8"
        doc.title "Nokogiri Builder"
      end
      doc.body do
        doc.h1 "Liste d'articles"
        doc.ul do
          %w[ruby html nokogiri].each do |name|
            doc.li { doc.a name.capitalize, href: "/tags/#{name}" }
          end
        end
      end
    end
  end

  [200, { "Content-Type" =&gt; "text/html; charset=utf-8" }, [builder.to_html]]
end

Rack::Handler::Puma.run app, Port: 9292
<br></pre>
<div>Pourquoi c'est pratique ? Boucles/conditions naturelles, markup toujours valide, et post‑traitement DOM facile.</div>
<h1>4) Phlex en mode <strong>mini‑rails</strong> : assets, routing, base de données</h1>
<div>On passe à un mini projet structuré pour se rapprocher d'une app web réelle.</div>
<div><strong>L'arborescence</strong></div>
<pre>mini-rails/
  Gemfile
  app.rb
  db/
    setup.rb
    development.sqlite3
  views/
    layout.rb
    posts_index.rb
    posts_show.rb
    home_page.rb
  public/
    style.css
    app.js
<br></pre>
<div><strong>Notre Gemfile</strong></div>
<pre>gem "rack"
gem "puma"
gem "phlex"
gem "activerecord"
gem "sqlite3"
<br></pre>
<div>bundle install</div>
<div>4.1 DB: Active Record sans Rails</div>
<div><strong>db/setup.rb</strong></div>
<pre>require "active_record"
ActiveRecord::Base.establish_connection(
  adapter: "sqlite3",
  database: File.expand_path("./db/development.sqlite3", __dir__)
)

unless ActiveRecord::Base.connection.table_exists?(:posts)
  ActiveRecord::Schema.define do
    create_table :posts do |t|
      t.string :title, null: false
      t.text :body, null: false
      t.timestamps
    end
  end
end

class Post &lt; ActiveRecord::Base; end
<br></pre>
<div>4.2 Vues Phlex</div>
<div><strong>views/layout.rb</strong></div>
<pre>require "phlex"

class Layout &lt; Phlex::HTML
  def initialize(<em>title:</em> "Mini-Rails"); @title = <em>title</em>; end

  def view_template
    doctype
    html do
      head do
        meta charset: "utf-8"
        title { @title }
        link rel: "stylesheet", href: "/style.css"
        script src: "/app.js"
      end
      body do
        header do
          h1 { "Mini-Rails" }
          nav do
            a(href: "/") { "Accueil" }
            plain " | "
            a(href: "/posts") { "Posts" }
          end
        end

        main do
          yield if block_given?
        end
      end
    end
  end
end
<br></pre>
<div><strong>views/posts_index.rb</strong></div>
<pre>require "phlex"
require_relative "layout"

class PostsIndex &lt; Phlex::HTML
  def initialize(<em>posts:</em>)
    @posts = <em>posts</em>
  end
  
  def view_template
    render Layout.new(title: "Posts") do
      h2 { "Articles" }
      
      if @posts.any?
        p { "Nombre d'articles: #{@posts.count}" }
        ul do
          @posts.each do |<em>post</em>|
            li do
              a(href: "/posts/#{<em>post</em>.id}") { <em>post</em>.title }
            end
          end
        end
      else
        p { "Aucun article pour le moment." }
        p { a(href: "#", onclick: "alert('Créez votre premier article!')") { "Créer le premier article" } }
      end
      
      h3 { "Nouvel article" }
      form(action: "/posts", method: "post") do
        div do
          label { "Titre" }
          br
          input(type: "text", name: "title", style: "width: 300px;")
        end
        div do
          label { "Contenu" }
          br
          textarea(name: "body", style: "width: 300px; height: 100px;")
        end
        div do
          button(type: "submit") { "Créer" }
        end
      end
    end
  end
end
<br></pre>
<div><strong>views/posts_show.rb</strong></div>
<pre>require "phlex"
require_relative "layout"

class PostsShow &lt; Phlex::HTML
  def initialize(<em>post:</em>); @post = <em>post</em>; end
  
  def view_template
    render Layout.new(title: @post.title) do
      article do
        h2 { @post.title }
        p { @post.body }
        p { a(href: "/posts") { "← Retour" } }
      end
    end
  end
end
<br></pre>
<div>4.3 Une page d'acceuil simple<br><strong>views/home_page.rb</strong>
</div>
<pre>require "phlex"
require_relative "layout"

class HomePage &lt; Phlex::HTML
  def view_template
    render Layout.new(title: "Accueil") do
      h2 { "Bienvenue !" }
      p  { "Ceci est la page d’accueil." }
      p  { a(href: "/posts") { "Voir les posts →" } }
    end
  end
end</pre>
<div>4.4 App + routing + assets</div>
<div><strong>app.rb</strong></div>
<pre>require "rack"
require "rack/handler/puma"
require "rack/files"
require_relative "db/setup"
require_relative "views/posts_index"
require_relative "views/posts_show"
require_relative "views/home_page"   <em># ← ajoute ceci</em>

PUBLIC_DIR = File.expand_path("./public", __dir__)

app = Proc.new do |<em>env</em>|
  req = Rack::Request.new(<em>env</em>)

  <em># Assets statiques</em>
  if req.path.start_with?("/style.css") || req.path.start_with?("/app.js")
    next Rack::Files.new(PUBLIC_DIR).call(<em>env</em>)
  end

  case [req.request_method, req.path]
  when ["GET", "/"]
    html = HomePage.new.call         <em># ← n’appelle plus Layout directement ici</em>
    [200, { "Content-Type" =&gt; "text/html; charset=utf-8" }, [html]]

  when ["GET", "/posts"]
    posts = Post.order(created_at: :desc).limit(50).to_a
    html  = PostsIndex.new(posts: posts).call
    [200, { "Content-Type" =&gt; "text/html; charset=utf-8" }, [html]]

  when ["POST", "/posts"]
    title = req.params["title"]&amp;.strip
    body  = req.params["body"]&amp;.strip
    if title.to_s.empty? || body.to_s.empty?
      next [422, { "Content-Type" =&gt; "text/html; charset=utf-8" }, ["Titre et contenu requis."]]
    end
    post = Post.create!(title: title, body: body)
    [302, { "Location" =&gt; "/posts/#{post.id}" }, []]

  else
    if req.get? &amp;&amp; req.path =~ %r{^/posts/(\d+)$}
      id   = req.path.match(%r{/posts/(\d+)$})[1]
      post = Post.find_by(id: id)
      next [404, { "Content-Type" =&gt; "text/html" }, ["Not found"]] unless post
      html = PostsShow.new(post: post).call
      [200, { "Content-Type" =&gt; "text/html; charset=utf-8" }, [html]]
    else
      [404, { "Content-Type" =&gt; "text/plain" }, ["Not found"]]
    end
  end
end

Rack::Handler::Puma.run app, Port: 9292
<br></pre>
<div>
<strong>public/style.css</strong> (exemple rapide)</div>
<pre>body {
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  line-height: 1.6;
  background-color: <em>#f5f5f5</em>;
}

header {
  border-bottom: 2px solid <em>#007bff</em>;
  padding-bottom: 15px;
  margin-bottom: 30px;
  background: <em>white</em>;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

h1 {
  color: <em>#007bff</em>;
  margin: 0;
}

nav {
  margin-top: 10px;
}

nav a {
  text-decoration: none;
  color: <em>#007bff</em>;
  font-weight: 500;
  margin-right: 10px;
}

nav a:hover {
  text-decoration: underline;
}

main {
  background: <em>white</em>;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  margin-bottom: 10px;
  padding: 10px;
  background: <em>#f8f9fa</em>;
  border-radius: 4px;
  border-left: 4px solid <em>#007bff</em>;
}

form {
  margin-top: 30px;
  padding: 20px;
  border: 1px solid <em>#ddd</em>;
  border-radius: 8px;
  background: <em>#f8f9fa</em>;
}

input,
textarea {
  width: 100%;
  max-width: 500px;
  margin-bottom: 15px;
  padding: 8px;
  border: 1px solid <em>#ccc</em>;
  border-radius: 4px;
  font-family: inherit;
}

button {
  background: <em>#007bff</em>;
  color: <em>white</em>;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background: <em>#0056b3</em>;
}

article {
  background: <em>white</em>;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

<br></pre>
<div>
<strong>public/app.js</strong> (exemple minimal)</div>
<pre>console.log("Mini‑Rails prêt !");
<br></pre>
<div>Lance bundle exec ruby app.rb et visite http://localhost:9292. Tu as maintenant:</div>
<ul>
<li>des <strong>assets</strong> servis depuis public/</li>
<li>un <strong>routing</strong> simple en fonction de la méthode et du chemin</li>
<li>un <strong>mini-blog</strong> avec <strong>SQLite + Active Record</strong> (création et lecture)</li>
</ul>
<h1>Conclusion</h1>
<div>On est partis d’un simple Proc Rack qui renvoie une page "Hello monde", et on a fini avec un mini-blog qui sert du HTML, du CSS, du JS, et qui cause à une base SQLite via Active Record. Le fil rouge ne bouge pas: on prend une requête, on renvoie [status, headers, body]. Entre les deux, on choisit l’outil qui va bien:</div>
<ul>
<li>string brute quand on veut juste tester un truc vite fait,</li>
<li>ERB pour rester proche du HTML,</li>
<li>Nokogiri pour générer du markup avec de la logique métier,</li>
<li>Phlex quand on veut des vues composables, testables, et une structure propre.</li>
</ul>
<div>L’idée n’est pas de réinventer Rails, mais de comprendre ce qu’il fait pour nous. Une fois qu’on a mis les mains dans Rack, env, les statuts HTTP et les en-têtes, Rails devient plus clair… et Ruby sans Rails devient moins intimidant.</div>
<div>
<strong>Et maintenant ?</strong><br>On te laisse deux pistes simples pour continuer:</div>
<ol>
<li>Ajouter l’édition et la suppression des articles (GET/POST /posts/:id/edit, DELETE /posts/:id).</li>
<li>Introduire des layouts/composants Phlex réutilisables (footer, flash messages), et un config.ru pour lancer avec rackup.</li>
</ol>
<div>Si tu tentes l’une des variantes, dis-nous ce que tu as bricolé. On est chauds pour en faire un suivi.</div>
<div><br></div>
<div>
<strong>Happy coding !</strong> 😄<br><br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi80NT9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--45147856af32b5839104d0a79f06377706742517" content-type="image/gif" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDUsInB1ciI6ImJsb2JfaWQifX0=--659d9ea2453ddd2f21d9b5747aa49b9427e44aa2/Excited%20Aww%20GIF.gif" filename="Excited Aww GIF.gif" filesize="5210363" width="384" height="480" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--gif">
    
    <img class="attachment--preview" src="https://res-4.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/z8eap3k3ooyg2138hwbmrxiff2jf">

</figure></action-text-attachment>
</div>
<div><br></div>
<div><br></div>
</div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/generer-du-html-avec-ruby-sans-rails</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/generer-du-html-avec-ruby-sans-rails</guid>
        <pubDate>Sun, 02 Nov 2025 15:44:39 +0100</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
            <category><![CDATA[web]]></category>
            <category><![CDATA[sqlite]]></category>
            <category><![CDATA[active_record]]></category>
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[templates]]></category>
            <category><![CDATA[html]]></category>
            <category><![CDATA[phlex]]></category>
            <category><![CDATA[nokogiri]]></category>
            <category><![CDATA[erb]]></category>
            <category><![CDATA[puma]]></category>
            <category><![CDATA[rack]]></category>
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Intermédiaire]]></category>
        
        <!-- Image principale -->
          <enclosure url="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDYsInB1ciI6ImJsb2JfaWQifX0=--9c6012cbe5fc445a44a1784bb4c79ec5789dec05/pexels-cottonbro-5052875.jpg" 
                     type="image/jpeg" 
                     length="2223461" />
        
        <!-- Métadonnées supplémentaires -->
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[8 min de lecture]]></category>
      </item>
      <item>
        <title><![CDATA[5 astuces pratiques pour la console Rails]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[1) Lancer la console en mode sandbox

Quand tu veux tester une manip sans risquer de salir la base, ajoute --sandbox.
bin/rails console --sandbox
R...]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <h1>1) Lancer la console en mode sandbox</h1><div>Quand tu veux tester une manip sans risquer de salir la base, ajoute --sandbox.</div><pre>bin/rails console --sandbox
<br></pre><div>Rails ouvre une transaction autour de ta session. Tu peux créer/modifier ce que tu veux, tout sera rollback à la sortie.</div><pre>User.create!(email: "test@example.com")
exit # =&gt; rollback auto
<br></pre><div>Tip: pratique aussi pour tester un migration ou un callback sans toucher aux vraies données.</div><h1>2) Recharger le code sans quitter: reload!</h1><div>Tu modifies un modèle ou un service pendant que la console tourne ? Pas besoin de quitter.</div><pre>reload!
<br></pre><div>Rails vide le cache de Zeitwerk et recharge les classes. Tu peux enchaîner tout de suite:</div><pre># avant reload! : la méthode n'existe pas
Article.new.respond_to?(:published_at_human) # =&gt; false

reload!

# après : chargée si tu as édité le fichier
Article.new.respond_to?(:published_at_human) # =&gt; true
<br></pre><div>Si un truc reste coincé, tu peux aussi tuer Spring:</div><pre>bin/spring stop
<br></pre><h1>3) Tester un endpoint sans curl: app.get et les helpers</h1><div>Depuis la console, tu peux appeler tes routes comme un mini navigateur.</div><pre>app.get "/status"
app.response.status     # =&gt; 200
app.response.media_type # =&gt; "application/json"
app.response.body       # =&gt; "{...}"
<br></pre><div>Besoin d'une URL ou d'un helper de vue ?</div><pre>app.url_helpers.post_path(Post.first)
helpers.number_to_currency(42)
<br></pre><div>Pratique pour débugger un contrôleur en 30 secondes.</div><h1>4) Faire taire le bruit SQL (ou l'afficher mieux)</h1><div>Quand la console spam trop, baisse le niveau de logs:</div><pre>ActiveRecord::Base.logger.level = :warn  # ou :error
<br></pre><div>Et pour revenir au niveau par défaut:</div><pre>ActiveRecord::Base.logger.level = :info
<br></pre><div>Autre option ponctuelle:</div><pre>Rails.logger.silence do
  heavy_query
end
<br></pre><h1>5) Eviter de charger 1 million de lignes en RAM</h1><div>Dans la console, on a vite fait un User.all.each fatal. Préfère ces patterns:</div><pre># 5a) Parcourir sans tout charger\ nUser.find_each(batch_size: 1000) { |u| u.touch }

# 5b) Travailler par paquets\ nUser.in_batches(of: 500) { |batch| batch.update_all(active: true) }

# 5c) Récupérer juste les colonnes utiles\ nUser.where(active: true).pluck(:id, :email)

# 5d) Voir la requête générée\ nUser.where(active: true).to_sql
<br></pre><div>Tu économises RAM, temps et sueur froide.</div><h1>6) Manipuler les résultats rapidement: pluck, pick, select vs map</h1><div>Tu veux récupérer des valeurs sans instancier des objets ActiveRecord (et sans exploser la RAM) ? pluck et pick sont tes amis.</div><div>pluck – retourne des valeurs Ruby, directement depuis SQL</div><ul><li><strong>Un attribut</strong></li><li><pre>User.where(active: true).pluck(:email)
# =&gt; ["a@ex.com", "b@ex.com", ...]
<br></pre></li><li><strong>Plusieurs attributs</strong> (renvoie des paires/tuples)</li><li><pre>User.pluck(:id, :email)
# =&gt; [[1, "a@ex.com"], [2, "b@ex.com"], ...]
<br></pre></li><li>Besoin d'un hash id =&gt; email ?</li><li><pre>User.pluck(:id, :email).to_h
# =&gt; {1=&gt;"a@ex.com", 2=&gt;"b@ex.com", ...}
<br></pre></li><li><strong>Avec distinct, order, limit</strong></li><li><pre>User.distinct.order(:created_at).limit(10).pluck(:email)
<br></pre></li><li><strong>Pourquoi c'est mieux que map(&amp;:email) ?</strong><br>map instancie chaque User en Ruby. pluck fait un SELECT email côté DB et renvoie des valeurs prêtes à l'emploi. C'est plus léger et souvent plus rapide.</li></ul><div>pick – la version "un seul enregistrement"</div><div>Récupère une ou plusieurs colonnes du <em>premier</em> enregistrement, sans objet AR :</div><pre>User.where(email: "a@ex.com").pick(:id)
# =&gt; 42

User.where(email: "a@ex.com").pick(:id, :created_at)
# =&gt; [42, 2025-07-21 10:03:00 +0200]
<br></pre><div>C'est l'équivalent compact de where(...).limit(1).pluck(...) .first, plus propre que find_by(... )&amp;.id.</div><div>select (SQL) vs map (Ruby)</div><ul><li>select(:id, :email) <strong>charge des objets AR</strong> mais uniquement avec les colonnes listées. Utile si tu dois <strong>appeler des méthodes</strong> ensuite.</li><li>map(&amp;:email) parcourt en Ruby ce qui est déjà en mémoire. Sur de gros volumes, privilégie find_each ou in_batches.</li></ul><div>Petits raccourcis utiles</div><pre>User.ids                  # ≈ User.pluck(:id)
User.where(active: true).pluck(:id, :email).map { |id, mail| { id: id, email: mail } }
user.slice(:id, :email)   # Hash avec seulement ces attributs
User.where(role: :admin).as_json(only: [:id, :email])
<br></pre><div>Tu as une astuce console fétiche ? Partage-la en commentaire.<br><br><br><br></div><div><strong>Happy coding !</strong> 😄<br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi80Mz9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--c0049e93037209b24599b8c2755f49e2cca514c4" content-type="image/gif" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDMsInB1ciI6ImJsb2JfaWQifX0=--8daa6a81fafaf3bab6a86a7a36b5016171c3488f/Happy%20Lets%20Go%20GIF%20by%20SpongeBob%20SquarePants.gif" filename="Happy Lets Go GIF by SpongeBob SquarePants.gif" filesize="414066" width="500" height="286" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--gif">
    
    <img class="attachment--preview" src="https://res-3.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/2m9mssqmxla1rjgg871x50qpabe2">

</figure></action-text-attachment></div><div><br></div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/5-astuces-pratiques-pour-la-console-rails</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/5-astuces-pratiques-pour-la-console-rails</guid>
        <pubDate>Tue, 07 Oct 2025 11:35:43 +0200</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
            <category><![CDATA[tips]]></category>
            <category><![CDATA[console]]></category>
            <category><![CDATA[rails]]></category>
            <category><![CDATA[active_record]]></category>
            <category><![CDATA[café_ruby]]></category>
            <category><![CDATA[debug]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[pluck]]></category>
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Intermédiaire]]></category>
        
        <!-- Image principale -->
          <!-- Image par défaut Unsplash -->
          <enclosure url="https://images.unsplash.com/photo-1416339684178-3a239570f315?q=80&amp;w=3774&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" 
                     type="image/jpeg" 
                     length="0" />
        
        <!-- Métadonnées supplémentaires -->
          <category><![CDATA[Série: Café Ruby]]></category>
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[3 min de lecture]]></category>
      </item>
      <item>
        <title><![CDATA[Créer et publier sa première gem Ruby avec slugfy_me : un tutoriel complet]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[🚀 Objectif de ce tutoriel



Créer une gem Ruby, la tester, l&#39;emballer et la publier sur RubyGems.org à travers un exemple simple et concret : slug...]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <h1>🚀 Objectif de ce tutoriel</h1><div><br><br>Créer une gem Ruby, la tester, l'emballer et la publier sur <a href="https://rubygems.org/">RubyGems.org</a> à travers un exemple simple et concret : slugfy_me, une mini-gem qui transforme une chaîne de caractères en un "slug" propre pour les URL.<br><br>Ce guide est fait pour toi si :</div><ul><li>Tu veux comprendre comment fonctionne une gem Ruby de l’intérieur</li><li>Tu rêves de publier ton propre code réutilisable</li><li>Tu aimes les projets pédagogiques bien guidés 😄</li></ul><div><br></div><h1>🧠 C'est quoi un "slug" ?</h1><div>Un <strong>slug</strong>, c’est une version simplifiée d’un titre ou d’une chaîne de caractères, souvent utilisée dans les URLs. Par exemple :</div><pre>"C'est quoi Ruby ?" devient =&gt; "cest-quoi-ruby"
<br></pre><div>C’est plus propre, plus lisible et meilleur pour le référencement.</div><h1>🏗️ Étape 1 - Générer la structure de la gem</h1><div>On utilise Bundler pour créer le squelette de notre gem :</div><pre>bundle gem slugfy_me
<br></pre><div>Réponds aux questions :</div><ul><li><strong>Test framework</strong> : rspec</li><li><strong>Licence</strong> : MIT</li><li><strong>README, changelog, code of conduct</strong> : oui</li></ul><div>Tu obtiens une arborescence complète avec :</div><ul><li>lib/slugfy_me.rb → le cœur du code</li><li>slugfy_me.gemspec → les infos de ta gem</li><li>spec/ → les tests</li></ul><h1>✍️ Étape 2 - Écrire la méthode slugify</h1><div>On veut une méthode simple :</div><pre>SlugfyMe.slugify("Crème brûlée au Café !")
# =&gt; "creme-brulee-au-cafe"
<br></pre><div>Elle va :</div><ul><li>Mettre en minuscules</li><li>Supprimer les caractères spéciaux</li><li>Remplacer les lettres accentuées</li><li>Remplacer les espaces par un tiret (ou un séparateur custom)</li></ul><div>Exemple de code minimal :</div><pre>module SlugfyMe
  def self.slugify(string, separator: '-')
    string = remove_accents(string)
    string.downcase
          .gsub(/[^a-z0-9\s]/, '')
          .strip
          .gsub(/\s+/, separator)
  end

  def self.remove_accents(string)
    accents = {
      'àáâãäå' =&gt; 'a',
      'ç'      =&gt; 'c',
      'èéêë'   =&gt; 'e',
      'ìíîï'   =&gt; 'i',
      'ñ'      =&gt; 'n',
      'òóôõö'  =&gt; 'o',
      'ùúûü'   =&gt; 'u',
      'ýÿ'     =&gt; 'y'
    }

    accents.each do |group, replacement|
      string = string.gsub(/[#{group}]/i, replacement)
    end

    string
  end
end
<br></pre><h1>🧪 Étape 3 - Tester avec RSpec</h1><div>Les tests se trouvent dans spec/slugfy_me_spec.rb. Exemples :</div><pre>RSpec.describe SlugfyMe do
  it "transforme une chaîne simple" do
    expect(SlugfyMe.slugify("Ruby rocks!")).to eq("ruby-rocks")
  end

  it "supprime les accents" do
    expect(SlugfyMe.slugify("Crème brûlée au Café")).to eq("creme-brulee-au-cafe")
  end

  it "utilise un séparateur personnalisé" do
    expect(SlugfyMe.slugify("Ruby est top", separator: '_')).to eq("ruby_est_top")
  end
end
<br></pre><div>Exécute les tests :</div><pre>bundle exec rspec
<br></pre><h1>📄 Étape 4 - Préparer la publication</h1><div>Modifier le .gemspec</div><div>Assure-toi que ton fichier slugfy_me.gemspec contient :</div><pre>spec.name        = "slugfy_me"
spec.version     = SlugfyMe::VERSION
spec.authors     = ["francilobbie"]
spec.email       = ["ton.email@email.com"]
spec.summary     = "An educational Ruby gem to generate clean slugs from strings"
spec.description = "SlugfyMe is a minimalist gem created for Captain Ruby, to help you learn how to build and publish a gem."
spec.homepage    = "https://github.com/francilobbie/slugfy_me"
spec.license     = "MIT"
<br></pre><h1>🔑 Étape 5 - Générer une clé API RubyGems.org</h1><div>Avant de publier, tu dois être connecté avec une clé API RubyGems.</div><div>Comment faire :</div><ol><li>Va sur <a href="https://rubygems.org/profile/edit">rubygems.org/profile/edit</a></li><li>Descends jusqu'à <strong>API access</strong> &gt; clique sur <strong>New API Key</strong></li><li>Donne un nom (ex : dev-laptop) et valide</li><li>Ensuite, dans ton terminal :</li></ol><pre>gem signin
<br></pre><div>Renseigne ton mail + mot de passe RubyGems, et la clé est automatiquement enregistrée dans ~/.gem/credentials.</div><div>C’est ce qui permet ensuite de publier sans retaper ton mot de passe à chaque fois ✅</div><h1>🚢 Étape 6 - Publier la gem sur RubyGems</h1><ol><li>Build la gem :</li></ol><pre>gem build slugfy_me.gemspec
<br></pre><ol><li>Push vers RubyGems :</li></ol><pre>gem push slugfy_me-0.1.0.gem
<br></pre><div>Et voilà ✨ Ta gem est en ligne ici :<br>&nbsp;👉 <a href="https://rubygems.org/gems/slugfy_me">https://rubygems.org/gems/slugfy_me</a></div><h1>📦 Code source sur GitHub</h1><div>Le code est open-source ici :<br>&nbsp;👉 <a href="https://github.com/francilobbie/slugfy_me">https://github.com/francilobbie/slugfy_me</a></div><div>Tu peux l’étudier, le forker, ou proposer des améliorations via une Pull Request.</div><h1>🎒 Aller plus loin</h1><div>Maintenant que tu sais créer une gem, pourquoi ne pas explorer celles que tu utilises déjà ?</div><div>🔍 bundle open : comprendre comment une gem fonctionne</div><div>La commande bundle open te permet d’<strong>ouvrir le dossier source d’une gem installée</strong> sur ta machine, dans ton éditeur par défaut (souvent VS Code ou Vim).</div><div>Par exemple :</div><pre>bundle open slugfy_me
<br></pre><div>Tu seras directement dans le dossier slugfy_me tel qu’il est installé sur ta machine, et tu pourras voir son code, ses tests, sa spec, etc.</div><div>📚 C’est un excellent moyen d’apprendre comment sont conçues les gems populaires (devise, sidekiq, pagy, etc.)</div><div>✨ Améliore ta gem !</div><div>Voici quelques pistes :</div><ul><li>Ajouter d’autres options (:upcase, :max_length, etc.)</li><li>Supporter plus d’accents (ou utiliser I18n.transliterate pour déléguer)</li><li>Ajouter une CLI (bin/slugfy) pour l’utiliser depuis le terminal</li><li>Ajouter une doc avec <a href="https://rubydoc.info/gems/yard">yard</a></li></ul><div>Et pourquoi pas en faire une vraie gem utile à partager ? 😎</div><h1>🧠 Conclusion</h1><div>Tu viens de :</div><ul><li>Créer une gem Ruby from scratch</li><li>Ajouter une logique utile</li><li>Écrire des tests</li><li>Documenter proprement</li><li>Publier sur RubyGems</li></ul><div>Pas mal pour un premier tour de chauffe, non ? 😄</div><div>N’hésite pas à te lancer sur ta propre gem, même toute simple. C’est l’un des meilleurs moyens d’apprendre Ruby autrement.<br><br><strong>Happy coding !</strong> 😄<br><br></div><div><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi80Mj9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--33221f1b86058c53b71379c5237dfbf07ba87536" content-type="image/gif" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDIsInB1ciI6ImJsb2JfaWQifX0=--33245feea76570cf73e1b0219ed5f9da8963d3d7/Excited%20Aww%20GIF.gif" filename="Excited Aww GIF.gif" filesize="5210363" width="384" height="480" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--gif">
    
    <img class="attachment--preview" src="https://res-2.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/edaijokugzmw1krrqvoeycvb0wxt">

</figure></action-text-attachment></div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/creer-et-publier-sa-premiere-gem-ruby-avec-slugfy_me-un-tutoriel-complet</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/creer-et-publier-sa-premiere-gem-ruby-avec-slugfy_me-un-tutoriel-complet</guid>
        <pubDate>Tue, 19 Aug 2025 07:46:00 +0200</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
            <category><![CDATA[dev débutant]]></category>
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[gem]]></category>
            <category><![CDATA[bundler]]></category>
            <category><![CDATA[ruby-on-rails]]></category>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[publication]]></category>
            <category><![CDATA[outils ruby]]></category>
            <category><![CDATA[captain ruby]]></category>
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Intermédiaire]]></category>
        
        <!-- Image principale -->
          <!-- Image par défaut Unsplash -->
          <enclosure url="https://images.unsplash.com/photo-1499750310107-5fef28a66643?q=80&amp;w=3870&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" 
                     type="image/jpeg" 
                     length="0" />
        
        <!-- Métadonnées supplémentaires -->
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[5 min de lecture]]></category>
      </item>
      <item>
        <title><![CDATA[Le paramètre &quot;it&quot; de Ruby 3.4]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[Introduction 🚀


Tu pensais avoir fait le tour des blocks ? Ruby 3.4 débarque et glisse un tout petit mot‑clé : it. Ce sucre syntaxique te permet d...]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <h1>Introduction 🚀</h1><div><br></div><div>Tu pensais avoir fait le tour des blocks ? Ruby 3.4 débarque et glisse un tout petit mot‑clé : <strong>it</strong>. Ce sucre syntaxique te permet d’accéder au <em>premier</em> argument d’un block sans avoir à le nommer. Ça paraît anecdotique, mais sur les one‑liners — et même sur certains blocks de quelques lignes — le gain en lisibilité est réel. On décortique tout ça ensemble, café à la main.<br><br><br><br>
</div><div><br></div><h1>Pourquoi it ? 🤔</h1><div><br></div><div>Chaque fois que tu écris un map, select ou reject, tu déclares souvent une variable fantôme — juste pour la réutiliser une ligne plus loin :</div><pre>numbers.map { |n| n * 2 }
<br></pre><div>Dans 100 % des cas où ton block ne prend qu’un paramètre, cette variable est redondante. Ruby 3.4 autorise désormais :</div><pre>numbers.map { it * 2 }
<br></pre><div>Le mot‑clé <strong>it</strong> représente automatiquement le premier paramètre implicite du block. Finies les variables jetables comme |x|, |e| ou |v|.<br><br><br><br><br>
</div><h1>Avant / Après détaillé 🪄</h1><div><br></div><div>Voici trois cas ultra‑courants, avant et après it, avec un mot d’explication à chaque fois :</div><div>
<br><strong>Cas d’usage</strong> <strong>Code </strong><strong><em>avant</em></strong><strong> (&lt;= 3.3)</strong> <strong>Code </strong><strong><em>après</em></strong><strong> (3.4)</strong> <strong>Ce que ça change</strong><br>| Filtrer les fichiers vides&nbsp; | files.reject { |f| f.empty? }&nbsp; | files.reject { it.empty? }&nbsp; | Plus besoin de nommer f, focus direct sur la condition.<br>| Trier des mots par longueur&nbsp; | words.sort_by { |w| w.length }&nbsp; | words.sort_by { it.length }&nbsp; | On retire des caractères <em>et</em> on lit « it.length » comme une phrase.<br>| Garder les posts publiés&nbsp; | posts.select { |p| p.published? }&nbsp; | posts.select { it.published? }&nbsp; | La logique saute aux yeux : « sélectionne quand <em>il</em> est publié ».<br><br><br>
</div><div>Autres cas sympas 💡</div><ul>
<li><strong>each_with_object</strong></li>
<li><pre>data.each_with_object({}) { it.process!(...) }
<br></pre></li>
<li>
<strong>tap</strong> pour du debug express</li>
<li><pre>result.tap { puts "DEBUG =&gt; #{it.inspect}" }
<br></pre></li>
<li>
<strong>Injections simples</strong> avec inject / reduce</li>
<li><pre>prices.reduce(0) { _1 + it.taxed }
# mélange possible avec _1 (1er param) + it (2ᵉ param)
<br></pre></li>
</ul><div><br></div><h1>Limites à connaître ⚠️</h1><ol>
<li><br></li>
<li>
<strong>Un seul paramètre implicite</strong>. Si ton block prend plusieurs arguments, it ne couvre que le premier ; les autres doivent être déclarés normalement.</li>
<li>
<strong>Lambdas “stabby”</strong>. Pas dispo dans -&gt; { ... } si tu n’indiques pas les paramètres :</li>
<li><pre># ❌ Ne marche pas
add = -&gt; { it + 2 }

# ✅ OK
add = -&gt;(it) { it + 2 }
<br></pre></li>
<li>
<strong>Lisibilité long format</strong>. Sur un block multi‑lignes avec plusieurs références à l’argument, un nom explicite reste roi.</li>
</ol><div><br></div><div><br></div><h1>Performances : zéro overhead 🏎️</h1><div>
<br>it n’est qu’un alias reconnu par le parser. Le bytecode généré est le même que pour |x|, donc aucun impact sur la vitesse ou l’usage mémoire. YJIT/JIT ne font pas la différence non plus. Bref : gratuit !</div><div>Tester vite fait 🕹️</div><pre># Installer la preview (si besoin)
rbenv install 3.4.0-preview1
rbenv shell 3.4.0-preview1
irb

["café", "ruby", "rocks"].map { it.upcase }
# =&gt; ["CAFÉ", "RUBY", "ROCKS"]
<br></pre><div>Ça marche ? Ça te plaît ? Dis‑moi en commentaire si tu vas l’adopter ou si tu préfères garder tes bons vieux |obj|.<br><br><br><br><br><br><br><br>
</div><div>
<strong>Happy coding !</strong> 😄<action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8zOT9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--e780a9ec6dd5d989e022eac0dfc395a7d856368f" content-type="image/gif" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzksInB1ciI6ImJsb2JfaWQifX0=--58fdeca9e3fa9f8eff98075d2842787998f3a5d4/Happy%20Lets%20Go%20GIF%20by%20SpongeBob%20SquarePants.gif" filename="Happy Lets Go GIF by SpongeBob SquarePants.gif" filesize="414066" width="500" height="286" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--gif">
    
    <img class="attachment--preview" src="https://res-5.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/bll7t2lfkv6tya3x921m050mcfwg">

</figure></action-text-attachment>
</div><div><br></div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/le-parametre-it-de-ruby-3-4</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/le-parametre-it-de-ruby-3-4</guid>
        <pubDate>Wed, 23 Jul 2025 18:59:57 +0200</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
            <category><![CDATA[caféruby]]></category>
            <category><![CDATA[syntax]]></category>
            <category><![CDATA[ruby34]]></category>
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[productivité]]></category>
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Débutant]]></category>
        
        <!-- Image principale -->
          <enclosure url="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6NDAsInB1ciI6ImJsb2JfaWQifX0=--281b15fa30209d9d523d347f140c1a75c7714b32/pexels-goumbik-574077.jpg" 
                     type="image/jpeg" 
                     length="1824208" />
        
        <!-- Métadonnées supplémentaires -->
          <category><![CDATA[Série: Café Ruby]]></category>
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[3 min de lecture]]></category>
      </item>
      <item>
        <title><![CDATA[3 gems que j&#39;installe dans (presque) tous mes projets Rails 🚀]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[Introduction 🔧


Chaque fois que je crée une nouvelle app Rails, je tape quasi machinalement les mêmes trois commandes : bundle add friendly_id, bu...]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <h1>Introduction 🔧</h1><div>
<br>Chaque fois que je crée une nouvelle app Rails, je tape quasi machinalement les mêmes trois commandes : <em>bundle add friendly_id</em>, <em>bundle add pagy</em>, <em>bundle add font_awesome5_rails</em>. Ces trois gems sont devenues mes outils de base : elles rendent mes URLs plus jolies, mes listes plus propres, et mes interfaces un peu plus fun, sans alourdir l'app. Dans cet article "Café Ruby", on les survole en moins de 5 minutes, café à la main.<br><br><br><br><br>
</div><h1>1. <em>friendly_id</em> : des URLs humaines</h1><div>Tu veux <em>/articles/mon-super-tutoriel</em> au lieu de <em>/articles/42</em> ? <em>friendly_id</em> fait le boulot en deux temps trois mouvements.<br><br><br>
</div><pre># Gemfile
gem "friendly_id", "~&gt; 5.4"

# Terminal
rails generate friendly_id
rails db:migrate</pre><div>
<br>Dans ton modèle :<br><br><br>
</div><pre>class Article &lt; ApplicationRecord
  extend FriendlyId
  friendly_id :title, use: :slugged
end</pre><div>
<br>Et voilà, tes URLs sont lisibles et ton SEO te dit merci.<br><br><br>
</div><h1>2. <em>pagy </em>: la pagination la plus légère du Far West</h1><div>
<em>pagy </em>a remplacé <em>kaminari </em>chez moi dès le premier essai : API limpide, zéro dépendance, et perfs au top.<br><br><br><br>
</div><pre># Gemfile
gem "pagy"

# Controller
include Pagy::Backend

def index
  @pagy, @articles = pagy(Article.all)
end

# View (ERB)
&lt;%= pagy_nav(@pagy) %&gt;</pre><div>
<br>Tu veux du Bootstrap, Bulma ou Tailwind ? Pagy a des helpers prêts à l'emploi.<br><br><br><br>
</div><h1>3. <em>font_awesome5_rails </em>: des icônes en deux lignes</h1><div>Parce qu'une petite icône "edit" ou "trash" change tout, <em>font_awesome5_rails</em> injecte Font Awesome sans te forcer à bricoler Webpack.<br><br><br>
</div><pre># Gemfile
gem "font_awesome5_rails"

# Layout (application.html.erb)
&lt;%= stylesheet_link_tag "font_awesome5" %&gt;

# Vue
&lt;%= fa_icon "edit" %&gt;</pre><div>
<br>Rapide, propre, parfait pour les back‑offices ou les dashboards internes.<br><br><br>
</div><h1>Conclusion ☕</h1><div>Trois gems, trois commandes, un gros gain de confort sur tous mes projets. Et toi, c'est quoi tes indispensables ? Balance tes pépites en commentaire, je suis toujours preneur.<br><br>Happy Coding ! 🚀<br><br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8yNT9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--b53923da73268a4d4f63ee06658a2b4c836247ba" content-type="image/gif" url="http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjUsInB1ciI6ImJsb2JfaWQifX0=--76d18fa037a752b450a1d8e55d7ff4f2cb1a5546/Back%20To%20School%20What%20GIF.gif" filename="Back To School What GIF.gif" filesize="468327" width="500" height="270" previewable="true" presentation="gallery"><figure class="attachment attachment--preview">
  <img width="500" height="270" src="http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjUsInB1ciI6ImJsb2JfaWQifX0=--76d18fa037a752b450a1d8e55d7ff4f2cb1a5546/Back%20To%20School%20What%20GIF.gif">
</figure></action-text-attachment>
</div><div><br></div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/3-gems-que-j-installe-dans-presque-tous-mes-projets-rails</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/3-gems-que-j-installe-dans-presque-tous-mes-projets-rails</guid>
        <pubDate>Mon, 30 Jun 2025 22:38:59 +0200</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
            <category><![CDATA[rails]]></category>
            <category><![CDATA[caféruby]]></category>
            <category><![CDATA[productivity]]></category>
            <category><![CDATA[gems]]></category>
            <category><![CDATA[ruby]]></category>
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Débutant]]></category>
        
        <!-- Image principale -->
          <enclosure url="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzYsInB1ciI6ImJsb2JfaWQifX0=--8df6238614cfadf77539bf2169c81b76d7fce16d/61c1d398bde7-article-main.jpg" 
                     type="image/jpeg" 
                     length="44659" />
        
        <!-- Métadonnées supplémentaires -->
          <category><![CDATA[Série: Café Ruby]]></category>
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[2 min de lecture]]></category>
      </item>
      <item>
        <title><![CDATA[Gérer les erreurs en Ruby : comprendre, déboguer, ne plus paniquer]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[
“ “There are two ways to write error-free programs; only the third one works.” — Alan J. Perlis ”


Introduction ⚙️

Quand on code en Ruby, on fin...]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <div><br></div><blockquote>&nbsp;“There are two ways to write error-free programs; only the third one works.” — Alan J. Perlis&nbsp;</blockquote><div>
<br><br><br>
</div><h1>Introduction ⚙️</h1><div>Quand on code en Ruby, on finit toujours par croiser des erreurs. Pas parce qu'on est mauvais, mais parce que le code qu'on écrit est vivant, souvent complexe, et parfois un peu capricieux. Et heureusement : les erreurs sont nos meilleurs signaux pour corriger, améliorer et comprendre notre code.</div><div>Ce guide est là pour t'apprendre à apprivoiser les exceptions Ruby :</div><ul>
<li>Comment elles fonctionnent ?</li>
<li>Quelles sont les plus courantes ?</li>
<li>Comment les lire intelligemment ?</li>
<li>Comment les gérer proprement ?</li>
<li>Comment créer ses propres erreurs utiles pour ton app ?</li>
</ul><div>Avec des exemples concrets, des bonnes pratiques, des nouveautés de Ruby 3.4.1, et même quelques retours de devs glanés sur Reddit. Bref, tout ce qu’il faut pour que les erreurs ne te fassent plus peur.<br><br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8zND9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--cded25134eaee5e2d2128f6b7759a196137ce4b5" content-type="image/jpeg" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzQsInB1ciI6ImJsb2JfaWQifX0=--f9ddc9d4cccc4b0ef4427a386e342fd81e3401ce/pexels-rdne-8363153.jpg" filename="pexels-rdne-8363153.jpg" filesize="2627795" width="6535" height="4357" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--jpg">
    
    <img class="attachment--preview" src="https://res-2.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/0jgt0ahqh4x46tes59n7u935taa3">

</figure></action-text-attachment><br><br><br><br><br>
</div><h1>I. C’est quoi une exception en Ruby ? 🧩</h1><div>Une <strong>exception</strong> est une situation anormale qui interrompt l’exécution normale d’un programme. Par exemple : diviser par zéro, appeler une méthode qui n'existe pas, ouvrir un fichier absent…</div><div>Ruby utilise des objets pour représenter ces erreurs. Chaque type d'erreur est une classe, souvent héritée de <em>StandardError</em>.<br><br><strong>Structure de base</strong>
</div><pre>begin
  # code potentiellement problématique
rescue
  # code exécuté en cas d’erreur
end</pre><div>
<br>Quand une erreur est levée, Ruby saute immédiatement au bloc <em>rescue</em> correspondant. Si aucun bloc ne correspond, l’exception se propage dans la pile jusqu’à ce que quelque chose la capture… ou que le programme plante.</div><div>Il existe donc deux grands types d'erreurs :</div><ul>
<li>
<strong>Silencieuses</strong> : capturées et gérées</li>
<li>
<strong>Fatales</strong> : non capturées, le programme s’arrête</li>
</ul><div>
<br><br><br><br><br>
</div><h1>II. Lire un message d’erreur : le stack trace expliqué 🐛</h1><div>Un message d'erreur Ruby, aussi appelé <strong>stack trace</strong>, est ton meilleur ami. Il te dit exactement :</div><ul>
<li>le <strong>fichier</strong> concerné</li>
<li>la <strong>ligne</strong> où ça a planté</li>
<li>le <strong>type d’erreur</strong>
</li>
<li>le <strong>message explicite</strong>
</li>
</ul><div>
<br><br><br><br><br>
</div><div>Exemple :</div><pre>main.rb:12:in `calculate': divided by 0 (ZeroDivisionError)</pre><div>
<br>Tu sais que dans <em>main.rb</em>, à la ligne 12, dans la méthode <em>calculate</em>, un <em>ZeroDivisionError</em> a été levé avec le message "divided by 0".<br><br><br><br><br>
</div><div>
<strong>Bonus sympa : </strong><strong><em>Did you mean?</em></strong><br><br><br><br><br>
</div><div>Depuis Ruby 2.3, Ruby te suggère souvent des corrections de nom :</div><pre>NameError: undefined local variable or method `usr' for main:Object
Did you mean?  user</pre><div>
<br>Une petite pépite pour corriger les fautes de frappe 😄<br><br><br><br><br>
</div><h1>III. Les erreurs les plus fréquentes 🧱</h1><div>Voici les erreurs que tu rencontreras le plus souvent. Pour chaque type : une explication, un exemple, une astuce.<br><br><br><br><br>
</div><div>
<strong>NameError</strong><br><br><br><br><br>
</div><div>Variable ou constante non définie.</div><pre>puts total_price
# =&gt; NameError: undefined local variable or method 'total_price'</pre><div>
<br><strong>Solution</strong> : vérifier la déclaration, la casse, ou la portée de ta variable.<br><br><br><br><br>
</div><div><strong>NoMethodError</strong></div><div>Méthode appelée sur un objet qui ne la connaît pas.</div><pre>user = nil
user.name
# =&gt; NoMethodError: undefined method `name` for nil:NilClass</pre><div>
<br><br><strong>Solution</strong> : bien vérifier que ton objet n’est pas <em>nil</em>, ou utiliser <em>&amp;.</em> (safe navigation operator).<br><br><br><br><br>
</div><div><strong>ArgumentError</strong></div><div>Le nombre ou type d’arguments ne colle pas.</div><pre>def greet(name, age); end
greet("Alice")
# =&gt; ArgumentError: wrong number of arguments (given 1, expected 2)</pre><div>
<br><strong>Solution</strong> : toujours relire la signature de la méthode.<br><br><br><br><br>
</div><div><strong>SyntaxError</strong></div><div>Code invalide au parsing.</div><pre>if true
  puts "ok"
# =&gt; SyntaxError: unexpected end-of-input, expecting keyword_end</pre><div>
<br><strong>Solution</strong> : fermer toutes tes structures (<em>end</em>) et valider ton code dans un éditeur.<br><br><br><br><br>
</div><div><strong>TypeError</strong></div><div>Type d’objet inapproprié.</div><pre>[1, 2, 3] + "salut"
# =&gt; TypeError: no implicit conversion of String into Array</pre><div>
<br><strong>Solution</strong> : toujours s'assurer que les objets sont compatibles.<br><br><br><br><br>
</div><div><strong>ZeroDivisionError</strong></div><div>La classique division par zéro 😬</div><pre>10 / 0
# =&gt; ZeroDivisionError: divided by 0</pre><div>
<br><strong>Solution</strong> : toujours vérifier les dénominateurs avant de diviser.<br><br><br><br><br>
</div><div><strong>LoadError / Errno::ENOENT / IOError</strong></div><div>Fichier ou ressource manquante.</div><pre>File.read("/path/to/missing/file")
# =&gt; Errno::ENOENT: No such file or directory</pre><div>
<br><strong>Solution</strong> : vérifier les chemins, les permissions, et tester leur existence avant appel.<br><br><br><br><br>
</div><h1>IV. Comment gérer les erreurs proprement 🛠️</h1><div>Ruby nous donne plusieurs outils pour intercepter, traiter, relancer ou ignorer les erreurs. Mais comme tout outil puissant, ça peut aussi mal tourner si c’est mal utilisé.<br><br><br><br><br>
</div><div>
<strong><em>rescue</em></strong><strong> avec variable</strong>
</div><div>C’est la base : capturer l’exception dans une variable pour l’analyser.</div><pre>begin
  risky_code
rescue =&gt; e
  puts e.class       # Type d'erreur
  puts e.message     # Message associé
  puts e.backtrace   # Détail des appels
end</pre><div>
<br>Tu peux ensuite logger, alerter, ou relancer l’erreur après traitement si besoin avec <em>raise e</em>.<br><br><strong><em>else</em></strong><strong> et </strong><strong><em>ensure</em></strong>
</div><ul>
<li>
<em>else</em> s’exécute si <strong>aucune</strong> erreur n’est levée</li>
<li>
<em>ensure</em> s’exécute <strong>dans tous les cas</strong>, erreur ou pas</li>
</ul><pre>begin
  puts "On tente un truc"
rescue =&gt; e
  puts "Erreur capturée : #{e.message}"
else
  puts "Tout s'est bien passé !"
ensure
  puts "Nettoyage systématique"
end</pre><div>
<br>C’est super utile pour fermer un fichier, libérer une ressource, ou logger une info de fin de traitement.<br><br><strong><em>retry</em></strong><strong> et </strong><strong><em>raise</em></strong><br><br>Tu peux parfois vouloir <strong>réessayer</strong> après une erreur ponctuelle (réseau, base de données, etc.). Ruby permet ça avec <em>retry</em>.</div><pre>tries = 0
begin
  fetch_remote_data
rescue NetworkTimeout =&gt; e
  tries += 1
  retry if tries &lt; 3
  raise "Échec après 3 tentatives"
end</pre><div>
<br>Mais attention à ne pas créer de boucle infinie. Mets des limites claires.<br><br><br><br><br>
</div><h1>V. Lever ses propres exceptions 🚨</h1><div>Lever une exception, c’est dire « stop, cette situation est invalide, je préfère interrompre l’exécution ici ».</div><div>
<strong>Avec </strong><strong><em>raise</em></strong><strong> simple</strong>
</div><pre>raise "Ce nom est invalide"</pre><div>
<br>C’est pratique pour signaler un problème métier ou un cas limite mal géré.<br><br><br><br><br>
</div><div><strong>Avec une classe d’exception personnalisée</strong></div><pre>class PermissionDenied &lt; StandardError; end

def delete_user(user)
  raise PermissionDenied, "Vous n'avez pas le droit de faire ça" unless user.admin?
end</pre><div>
<br>Créer tes propres classes rend les erreurs <strong>plus explicites</strong> et facilite leur gestion (tu peux <em>rescue PermissionDenied</em> uniquement).<br><br><br><br><br>
</div><div><strong>En ajoutant des attributs</strong></div><pre>class ApiError &lt; StandardError
  attr_reader :code

  def initialize(message, code)
    super(message)
    @code = code
  end
end</pre><div>
<br>Tu peux ainsi accéder à plus d’infos quand tu traites l’erreur (<em>e.code</em>). Très utile pour les appels HTTP ou les erreurs de validation métier.<br><br><br><br><br>
</div><h1>VI. L’arbre des exceptions Ruby 🌳</h1><div>Ruby organise toutes ses erreurs en arbre. Tu peux en voir un extrait ici :<br><br><br><br><br>
</div><pre>Exception
├── NoMemoryError
├── ScriptError
│   ├── LoadError
│   ├── SyntaxError
├── SignalException
│   └── Interrupt
├── StandardError
│   ├── ArgumentError
│   ├── ZeroDivisionError
│   ├── RuntimeError
│   ├── NoMethodError
│   ├── etc.</pre><div>
<br>Pourquoi c’est important ? Parce que Ruby <strong>ne capture que StandardError par défaut</strong>.<br><br><br><br><br>
</div><div>Donc si tu fais <em>rescue</em>, ça ne prendra pas une <em>SyntaxError</em> ou un <em>SystemExit</em>. C’est voulu : ça évite de masquer les plantages critiques.<br><br><br><br><br>
</div><div>Moralité : hérite toujours de <em>StandardError</em> pour tes classes d’erreur perso.<br><br><br><br><br>
</div><h1>VII. Nouveautés et changements dans Ruby 3.4.1 🧪</h1><div>La gestion des exceptions a évolué avec les versions récentes.<br><br><br><br><br>
</div><div>
<strong><em>full_message</em></strong><br><br><br><br><br>
</div><div>Depuis Ruby 2.6, tu peux utiliser <em>e.full_message</em> pour une sortie lisible et colorée :</div><pre>rescue =&gt; e
  puts e.full_message(highlight: true)</pre><div>
<br>Pratique pour le debug sans avoir à parser <em>backtrace</em> manuellement.<br><br><br><br><br>
</div><div>
<strong><em>cause</em></strong><br><br><br><br><br>
</div><div>Si tu relèves une erreur dans un <em>rescue</em>, Ruby garde une trace de l’originale :</div><pre>begin
  raise "Erreur A"
rescue =&gt; e
  raise "Erreur B"
end</pre><div>
<br><em>e.cause</em> permet de suivre la chaîne d’erreurs. Top pour les logs ou les outils d’observabilité.<br><br><br><br><br>
</div><div>
<strong>Améliorations internes</strong><br><br><br><br><br>
</div><div>Ruby 3.4 a aussi clarifié les messages dans les lambdas, les blocs, et les appels indirects. Tu devrais voir moins de "undefined method" bizarres et plus d’infos utiles.<br><br><br><br><br>
</div><h1>VIII. Bonnes pratiques ✅</h1><div><br></div><div>Voici quelques règles simples pour éviter les pièges les plus courants :</div><ul>
<li>
<strong>Ne fais jamais </strong><strong><em>rescue nil</em></strong> : tu masques l’erreur, tu ne la règles pas.</li>
<li>
<strong>Ne rescue pas </strong><strong><em>Exception</em></strong> sauf cas ultra spécifiques (comme un moteur de sandbox).</li>
<li>
<strong>Loggue toujours</strong> ce que tu interceptes (au moins <em>e.message</em>).</li>
<li>
<strong>Utilise </strong><strong><em>ensure</em></strong> pour nettoyer après une opération critique (fichier, DB, etc.).</li>
<li>
<strong>Crée des erreurs personnalisées</strong> avec des noms clairs : <em>InvalidCredentials</em>, <em>RateLimitExceeded</em>, etc.</li>
</ul><div>Ces petites habitudes font une énorme différence dans la stabilité et la lisibilité de ton code.<br><br><br><br><br>
</div><h1>IX. Bonus : outils de debug Ruby 🧰</h1><div><br></div><div>Quelques pépites pour t'aider à mieux comprendre et corriger tes erreurs Ruby :</div><ul>
<li>
<strong>pry-rescue</strong> : ouvre un REPL automatiquement dès qu'une exception se produit.</li>
<li>
<strong>byebug</strong> : très utile pour mettre un breakpoint dans un <em>rescue</em>.</li>
<li>
<strong>binding.irb</strong> ou <strong>binding.pry</strong> : explore l'état du programme en live.</li>
<li>
<strong>exception.full_message(highlight: true)</strong> : affiche le message d’erreur avec couleurs et contexte.</li>
<li>
<strong>Sentry / Rollbar / Bugsnag</strong> : pour suivre les exceptions en production.</li>
</ul><div>
<br><br><br><br><br>
</div><h1>Conclusion 🎬</h1><div><br></div><div>Les erreurs Ruby ne sont pas là pour t'embêter. Elles sont là pour te parler.</div><div>Apprendre à les lire, les comprendre et les utiliser fait de toi un meilleur développeur. Et Ruby a l’avantage de rendre ce processus vraiment agréable, comparé à d’autres langages plus obscurs.</div><div>Prends le temps d’écouter ce que l’exception essaie de te dire.</div><div>Et surtout, continue à casser ton code. C’est comme ça qu’on apprend.<br><br><br><br><br>
</div><div>Happy coding ! 🚀<br><br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8zNT9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--9263133c083a07df30745cf3fd7459617f945d6f" content-type="image/gif" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzUsInB1ciI6ImJsb2JfaWQifX0=--d051ca9156e490af19c31f8892ae35e23244efdb/Back%20To%20School%20What%20GIF.gif" filename="Back To School What GIF.gif" filesize="468327" width="500" height="270" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--gif">
    
    <img class="attachment--preview" src="https://res-4.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/j1ny1h0xj6yw2oseh937uomd4uok">

</figure></action-text-attachment>
</div><div><br></div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/gerer-les-erreurs-en-ruby-comprendre-deboguer-ne-plus-paniquer</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/gerer-les-erreurs-en-ruby-comprendre-deboguer-ne-plus-paniquer</guid>
        <pubDate>Wed, 28 May 2025 17:40:41 +0200</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
            <category><![CDATA[exceptions]]></category>
            <category><![CDATA[ruby]]></category>
            <category><![CDATA[rails]]></category>
            <category><![CDATA[goodpractices]]></category>
            <category><![CDATA[erreurs]]></category>
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Intermédiaire à Confirmé]]></category>
        
        <!-- Image principale -->
          <enclosure url="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzMsInB1ciI6ImJsb2JfaWQifX0=--3aac7d4259657b0461131f02ebcaddc73a7e542e/pexels-brettjordan-7952673.jpg" 
                     type="image/jpeg" 
                     length="1036907" />
        
        <!-- Métadonnées supplémentaires -->
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[8 min de lecture]]></category>
      </item>
      <item>
        <title><![CDATA[HAML vs ERB : quel moteur de template choisir pour vos vues Rails ?]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[🚀 Introduction


Lorsque l&#39;on travaille avec Ruby on Rails, le moteur de templates par défaut est ERB (Embedded Ruby). Il est simple, robuste, et t...]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <h1><strong>🚀 Introduction</strong></h1><div>
<br><br>
</div><div>Lorsque l'on travaille avec Ruby on Rails, le moteur de templates par défaut est <strong>ERB (Embedded Ruby)</strong>. Il est simple, robuste, et très bien intégré dans l'écosystème Rails. Pourtant, au fil des années, des alternatives ont émergé, avec des promesses alléchantes : code plus lisible, syntaxe plus concise, meilleure productivité. C’est le cas de <strong>HAML</strong>, très prisé dans certaines équipes pour la clarté de ses vues.</div><div>Mais alors, faut-il adopter HAML ? Rester sur ERB ? Ou envisager d’autres options comme <strong>Slim</strong> ? Cet article propose un comparatif clair, complet et enrichi de retours d'expérience pour vous aider à choisir le moteur de templates le plus adapté à votre projet Rails.</div><div>🧵 <strong>À noter :</strong> Deux discussions riches sur <a href="https://www.reddit.com/r/ruby/comments/yl1pfs/erb_rubyonrails_vs_haml/">Reddit</a> et <a href="https://www.reddit.com/r/rails/comments/ak3tmx/htmlerb_vs_htmlhaml_vs_htmlslim_which_one_do_you/">celle-ci sur r/rails</a> ont permis de collecter des dizaines d’avis contrastés, vécus concrets, et pistes de réflexion pertinentes. Elles sont citées tout au long de l’article.<br><br>
</div><div><br></div><div><br></div><h1><strong>📄 ERB vs HAML : présentation</strong></h1><div><br></div><div>
<strong>ERB : le standard<br><br></strong>ERB (Embedded Ruby) est intégré nativement dans Rails depuis ses débuts. Il permet d’insérer du code Ruby dans du HTML via des balises <em>&lt;% %&gt;</em> (exécution) ou <em>&lt;%= %&gt;</em> (affichage)<br><br>
</div><div><br></div><pre>&lt;% if user.admin? %&gt;
  &lt;p&gt;Bienvenue, admin &lt;%= user.name %&gt; !&lt;/p&gt;
&lt;% end %&gt;</pre><div>
<br><strong>🔹 Avantages :</strong><br><br>
</div><ul>
<li>Syntaxe simple et familière pour les développeurs web</li>
<li>Flexibilité totale sur le HTML</li>
<li>Aucune dépendance externe</li>
<li>Intégration native dans tous les helpers Rails</li>
<li>Compatible avec tous les éditeurs et outils front (ex. Tailwind, Hotwire, Stimulus)</li>
</ul><div>
<br>🗣️ <strong>Ce que dit la communauté</strong> :<br><br>
</div><blockquote>"ERB parce que c'est du HTML. Pas besoin d’apprendre un nouveau DSL." -- <em>r/rails<br></em>"Plus proche du métal. Je n’ai jamais ressenti le besoin d’un autre langage de template." -- <em>BananafestDestiny</em>
</blockquote><div>
<br><strong>HAML : l’alternative concise<br></strong><br>HAML (HTML Abstraction Markup Language) adopte une syntaxe inspirée de YAML et Python, où l’indentation définit la structure. Les balises HTML sont remplacées par des préfixes (<em>%</em>,<em> . </em>ou <em>#</em>), rendant le code plus compact.<br><br>
</div><pre>- if user.admin?
  %p Bienvenue, admin #{user.name} !</pre><div>
<br>🔹 <strong>Avantages :</strong><br><br>
</div><ul>
<li>Code plus lisible et aéré</li>
<li>Moins de bruit visuel (pas de balises HTML explicites à fermer)</li>
<li>Moins d’erreurs de nesting et balises non fermées</li>
<li>Structure visuelle claire facilitant la relecture</li>
<li>Très apprécié dans des équipes expérimentées ou sur des bases de code legacy</li>
</ul><div>
<br>🗣️ <strong>Avis partagés :<br></strong><br><br>
</div><blockquote>"Je ne supporte plus les % partout, je suis passé à Slim." -- <em>pupdogg007</em><br>"HAML, tout simplement parce que je ne veux plus jamais fermer une balise à la main." -- <em>GozerDestructor</em><br>"J'ai utilisé HAML pendant des années avant de revenir à ERB pour sa simplicité." -- <em>Katafrakt</em>
</blockquote><div>
<br><br>
</div><div>
<br><br>
</div><h1><strong>✅ Productivité &amp; lisibilité</strong></h1><div>
<strong><br></strong>HAML est conçu pour être <strong>lisible et rapide à écrire</strong>. Une fois la syntaxe assimilée, il permet de gagner du temps et de produire des vues plus compactes. En supprimant les balises HTML explicites, il limite les erreurs et rend la structure du document plus visible.</div><div>Cependant, cette syntaxe peut perturber certains profils, notamment les intégrateurs HTML/CSS, peu habitués à cette logique ou aux langages basés sur l’indentation.<br><br>🔎 En pratique :<br><br>
</div><ul>
<li>HAML améliore la relecture des PRs, surtout dans les gros projets</li>
<li>ERB est plus tolérant et plus proche du HTML natif, facilitant la collaboration avec des profils non Ruby</li>
<li>Le copier-coller depuis des ressources HTML (StackOverflow, Tailwind docs…) est beaucoup plus naturel en ERB</li>
</ul><div>
<br>💬 <strong>Citations Reddit :<br></strong><br><br>
</div><blockquote>"ERB est verbeux, mais je peux y coller du HTML directement sans me poser de questions." -- <em>katafrakt</em>"HAML est génial jusqu’à ce que je doive convertir manuellement un snippet HTML copié sur un site." -- <em>u/[supprimé]</em>
</blockquote><div>
<br><br><strong>⏱️ Performances<br><br></strong>Côté performances, les différences sont modestes, mais notables selon les contextes :<br><br>
</div><ul>
<li>
<strong>ERB</strong> compile plus rapidement car intégré nativement via <em>erubi</em>
</li>
<li>
<strong>HAML</strong> et <strong>Slim</strong> utilisent <em>temple</em>, ce qui ajoute une couche de compilation</li>
</ul><div>
<br>📊 Benchmarks comparatifs :<br><br>
</div><ul>
<li><a href="https://github.com/cmer/haml-slim-erb">haml-slim-erb benchmark</a></li>
<li>La dernière version de HAML est beaucoup plus performante qu’avant (voir <a href="https://github.com/haml/haml/releases/tag/v6.0.0.beta.1">release notes HAML 6.0</a>)</li>
</ul><div>
<br>⚠️ À noter : certains logs de développement montrent une allocation mémoire plus élevée pour HAML. Mais en mode production, les vues sont compilées et mises en cache. Cet écart devient donc négligeable sauf si vous traitez des milliers de vues dynamiques à la volée.<br><br>🧪 <strong>Expérience terrain :<br></strong><br><br>
</div><blockquote>"Le nombre d’objets alloués explose avec HAML dans les logs dev, mais en prod, c’est négligeable." — <em>Icy_Mathematician182</em>
</blockquote><div>
<br><br><strong>🌟 Slim et au-delà<br><br></strong>Parmi les alternatives modernes, <strong>Slim</strong> sort du lot avec sa syntaxe épurée et sa vitesse d’exécution. C’est une évolution naturelle pour ceux qui trouvent HAML encore trop verbeux.<br><br>
</div><pre>- if user.admin?
  p Bienvenue, admin #{user.name} !</pre><div>
<br>📌 Points forts :<br><br>
</div><ul>
<li>Syntaxe ultra minimaliste</li>
<li>Parsing très rapide</li>
<li>Compatible Tailwind, Turbo et Hotwire</li>
</ul><div>
<br>🗣️ <strong>Témoignages Reddit :<br></strong><br><br>
</div><blockquote>"Chaque fois que je commence une nouvelle app avec ERB, je finis par revenir à Slim."<br><br>"Slim est un HAML raffiné : plus lisible, plus rapide."<br><br>"Le parser Slim est frustrant : la moindre erreur d’indentation renvoie une page blanche."</blockquote><div>
<br><br>
</div><div>Slim reste un choix excellent si :<br><br>
</div><ul>
<li>Votre équipe est expérimentée</li>
<li>Vous souhaitez une syntaxe minimaliste</li>
<li>Vous utilisez beaucoup de composants et de partials</li>
</ul><div>
<br><strong>🧩 En bonus : et les composants ?<br><br></strong>Un fil intéressant de la discussion évoque aussi des moteurs de templates 100 % Ruby comme <strong>Phlex</strong> ou <strong>ViewComponent</strong>, qui s’éloignent complètement du HTML en faveur d’une DSL purement Ruby.</div><div>📚 Ces approches permettent de modéliser les vues comme des objets Ruby testables, modulaires, et hautement réutilisables.<br><br>🧠 Extrait de la communauté :<br><br>
</div><blockquote>"Phlex est plus rapide qu’ERB. C’est fun, mais pas fait pour tout le monde.""Si je veux écrire du HTML avec des classes Ruby, je fais du markdown ou j’utilise des composants."</blockquote><div>
<br>🎯 À surveiller de près : Phlex est jeune, mais prometteur pour des projets orientés composants ou des librairies design systems maison.<br><br><strong>🔢 Synthèse<br></strong><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8yOD9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--5f05d790c404808d88ab6b35ef1ae333a9b21188" content-type="image/png" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjgsInB1ciI6ImJsb2JfaWQifX0=--650b3f97d3ec677601ec5b3cee1d82a768d76290/n9l5ndj2xub4tolbmenh2ra8ec3q.png" filename="n9l5ndj2xub4tolbmenh2ra8ec3q.png" filesize="105530" width="1771" height="882" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--png">
    
    <img class="attachment--preview" src="https://res-1.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/gr4swzbaemx7lhchbr3wr7g1pmjh">

</figure></action-text-attachment>
</div><h1><strong>🚀 Conclusion</strong></h1><div>
<strong><br></strong>Chaque moteur de template a ses forces et faiblesses. Si vous démarrez un projet Rails ou souhaitez simplifier votre stack : <strong>restez sur ERB</strong>. Il est standard, bien documenté, et compatible avec tout l'écosystème Rails 7.1.</div><div>Mais si vous cherchez une syntaxe plus expressive, que votre équipe maîtrise bien, et que vous voulez des vues ultra lisibles, <strong>HAML (ou Slim)</strong> peuvent être d’excellentes alternatives.</div><div>👀 Et si vous cherchez une rupture de paradigme, les moteurs comme <strong>Phlex</strong> ou <strong>ViewComponent</strong> offrent une approche entièrement Ruby et orientée composants.<br><br>💬 <strong>En résumé :</strong><br><br>
</div><ul>
<li>
<strong>ERB</strong> : simple, fiable, universel</li>
<li>
<strong>HAML</strong> : lisible, élégant, adapté aux gros projets</li>
<li>
<strong>Slim</strong> : minimaliste, rapide, pour les amoureux de la concision</li>
</ul><div>
<br><br>
</div><div>💡 Mon conseil : testez les trois sur un même template et comparez la lisibilité, la maintenance, et la productivité dans votre contexte.<br><br>
</div><div>&nbsp;Happy coding !&nbsp;</div><div>
<br><br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8yOT9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--35e96c79d737775e2d37f1818a83e8b9939c9922" content-type="image/gif" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjksInB1ciI6ImJsb2JfaWQifX0=--fa7488310b095bb99b6a1c35adc202856208dde9/Back%20To%20School%20What%20GIF.gif" filename="Back To School What GIF.gif" filesize="468327" width="500" height="270" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--gif">
    
    <img class="attachment--preview" src="https://res-5.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/hc5lc2drmt8wm8w8p3xpawb21crd">

</figure></action-text-attachment>
</div><div><br></div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/haml-vs-erb-quel-moteur-de-template-choisir-pour-vos-vues-rails</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/haml-vs-erb-quel-moteur-de-template-choisir-pour-vos-vues-rails</guid>
        <pubDate>Wed, 07 May 2025 10:46:07 +0200</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Intermédiaire à Confirmé]]></category>
        
        <!-- Image principale -->
          <enclosure url="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjUsInB1ciI6ImJsb2JfaWQifX0=--2f5966dbeee5ad50ec4bf08bf6ee2be8a895104e/pexels-padrinan-1591061.jpg" 
                     type="image/jpeg" 
                     length="1392700" />
        
        <!-- Métadonnées supplémentaires -->
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[6 min de lecture]]></category>
      </item>
      <item>
        <title><![CDATA[Construire des APIs modernes avec Ruby on Rails : REST, JSON, GraphQL, Turbo &amp; plus encore]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[Introduction
Dans le développement web moderne, construire une API robuste et bien pensée est devenu un prérequis pour tout projet sérieux. Ruby on...]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <div><strong>Introduction<br></strong><br></div><div>Dans le développement web moderne, construire une API robuste et bien pensée est devenu un prérequis pour tout projet sérieux. Ruby on Rails, souvent associé au développement HTML full-stack, offre en réalité une palette complète pour concevoir des APIs puissantes : REST, GraphQL, WebSockets, Turbo…</div><div>Rails 7.1 nous fournit une infrastructure élégante et cohérente qui permet de choisir parmi plusieurs architectures API, sans jamais sacrifier la lisibilité ni la productivité. Cet article s’adresse aux développeurs Ruby intermédiaires à avancés souhaitant approfondir leurs compétences dans la conception d’APIs professionnelles.<br><br><strong>1. Rails en mode API-first : minimalisme assumé<br></strong><br>Depuis Rails 5, le mode --api permet de générer une application allégée :<br><br></div><pre>rails new my_api_app --api</pre><div><br>Ce mode supprime les vues, assets, helpers HTML, middleware CSRF, et tout ce qui ne sert pas une API JSON. Résultat : une application plus rapide au démarrage, plus claire dans sa structure, et mieux adaptée aux API-first architectures.</div><div>🔍 Le contrôleur hérite de ActionController::API, avec une stack HTTP optimisée.</div><div>💡 À noter : vous pouvez toujours réactiver certains composants si nécessaire (views, middleware supplémentaires, etc.).</div><div>📦 Exemple d’ajout du middleware Rack::Cors pour autoriser les requêtes cross-origin :<br><br></div><pre># config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: [:get, :post, :patch, :put, :delete, :options]
  end
end</pre><div><br><br><strong>2. Construire une API REST propre, maintenable et évolutive<br><br></strong>REST est la pierre angulaire des APIs bien conçues. Rails suit les conventions REST de manière très intuitive.<br><br></div><div>Structure des routes :<br><br></div><pre>namespace :api do
  namespace :v1 do
    resources :articles
  end
end</pre><div><br>Dans vos contrôleurs :<br><br></div><pre>class Api::V1::ArticlesController &lt; ApplicationController
  def index
    articles = Article.all
    render json: ArticleSerializer.new(articles).serializable_hash
  end
end</pre><div><br>💎 Pour une architecture plus riche :</div><ul><li>Ajoutez une couche de services (e.g. <em>ArticleFetcherService</em>) pour isoler la logique métier.</li><li>Utilisez des policies (<em>Pundit</em>) pour sécuriser l'accès aux données.</li></ul><div>🔧 Serialisation JSON :</div><ul><li><em>Blueprinter</em> (rapide, lisible, flexible)</li><li><em>ActiveModel::Serializers</em> (plus verbeux, mais structurant)</li><li><br></li></ul><div>Pagination :<br><br></div><pre>include Pagy::Backend
@pagy, @articles = pagy(Article.all)</pre><div><br>Utilisez des scopes de tri et de recherche avec <em>ransack </em>:<br><br></div><pre>Article.ransack(title_cont: "Rails").result</pre><div><br>➡️ Pensez aussi au versionnement de votre API, à la gestion des erreurs centralisée, et à une documentation claire (cf. rswag).<br><br><br><strong>3. GraphQL avec graphql-ruby : précision et expressivité</strong><br><br></div><div>GraphQL permet aux clients de spécifier exactement les données attendues. C’est une alternative puissante à REST quand les besoins en données sont complexes ou très variables.<br><br></div><div>Installation :<br><br></div><pre>bundle add graphql
rails generate graphql:install</pre><div><br>Exemple de type :<br><br></div><pre>class Types::ArticleType &lt; Types::BaseObject
  field :id, ID, null: false
  field :title, String
  field :content, String
end</pre><div><br>Requête client :<br><br></div><pre>query {
  articles { id title content }
}</pre><div><br>🔥 Points forts :<br><br></div><ul><li>Flexibilité du client</li><li>Moins de sur-fetching</li><li>Documentation générée automatiquement (GraphiQL)</li><li><br></li></ul><div>⚠️ Attention :<br><br></div><ul><li>Implémentation plus lourde côté backend</li><li>Caching plus complexe</li></ul><div><br>💡 Astuce : utilisez GraphQL::Dataloader pour éviter les requêtes N+1.<br><br><br><strong>4. WebSockets avec Action Cable : vers une API en temps réel<br><br></strong>Action Cable est intégré nativement à Rails pour gérer les WebSockets. Pratique pour les notifications, chats, collaborations en direct.<br><br></div><div>Exemple de canal :<br><br></div><pre>class NotificationsChannel &lt; ApplicationCable::Channel
  def subscribed
    stream_from "notifications_#{current_user.id}"
  end
end</pre><div><br>Diffusion :<br><br></div><pre>Turbo::StreamsChannel.broadcast_append_to(
  "notifications_#{user.id}",
  target: "notifications",
  partial: "notifications/item",
  locals: { notification: Notification.last }
)</pre><div><br>Combinez cela avec Turbo Streams pour une interface ultra réactive, sans rechargement de page.<br><br><br><strong>5. Turbo &amp; Hotwire : l’API HTML intelligente<br></strong><br>Turbo Streams transforme votre HTML serveur en API efficace. Chaque interaction peut renvoyer un fragment HTML prêt à s’insérer dynamiquement dans le DOM.<br><br></div><div>Exemple : ajout de commentaire en live :<br><br></div><pre>&lt;%= turbo_frame_tag "comment_form" do %&gt;
  &lt;%= form_with model: @comment %&gt;
&lt;% end %&gt;

&lt;%= turbo_stream.append "comments" do %&gt;
  &lt;%= render @comment %&gt;
&lt;% end %&gt;</pre><div><br>✅ Bénéfices :<br><br></div><ul><li>UX fluide sans JS custom</li><li>Requête standard HTTP</li><li>Moins de logique côté frontend</li></ul><div><br>🔗 Combinez avec Stimulus pour des comportements enrichis.<br><br><br><strong>6. Authentification et sécurité : protéger votre API<br></strong><br>Plusieurs stratégies existent pour authentifier les requêtes API :</div><div>🔐 Auth Token simple : <em>Devise Token Auth</em> 🔐 JWT : <em>warden-jwt_auth, devise-jwt</em> 🔐 OAuth2 : <em>Doorkeeper </em>(authentification tierce, scopes, expiration…)<br><br></div><div>Sécurisation des endpoints :<br><br></div><pre>class Api::V1::BaseController &lt; ApplicationController
  before_action :authenticate_user!
end</pre><div><br>🛡 Pensez aussi à :<br><br></div><ul><li><em>rack-attack</em> pour limiter les abus</li><li><em>secure_headers </em>pour durcir la sécurité HTTP</li><li><em>auditing/logging</em> des accès sensibles</li></ul><div><br><br><strong>7. Tester son API : pilier de la robustesse<br></strong><br>Les tests garantissent la stabilité et la non-régression de votre API.</div><div>RSpec avec des <em>request specs</em> :<br><br></div><pre>describe "GET /api/v1/articles" do
  it "returns articles" do
    get "/api/v1/articles"
    expect(response).to have_http_status(:ok)
  end
end</pre><div><br>Complétez avec :<br><br></div><ul><li><em>FactoryBot </em>pour la fabrication de données</li><li><em>JSON helpers</em> pour simplifier les assertions</li><li><em>rswag </em>pour documenter l’API en Swagger</li><li><em>VCR/WebMock</em> pour mocker les appels externes</li></ul><div><br>➡️ Objectif : CI rapide, couverture complète, spécifications vivantes.<br><br><br><strong>Conclusion : Rails, un framework API à tout faire<br><br></strong>Rails ne se limite plus au rendu HTML. Il devient un vrai socle pour bâtir des architectures API modernes, scalables et maintenables. REST, GraphQL, WebSockets, HTML dynamique – tout est possible sans plugin exotique, sans surcharge technique.<br><br></div><div>👉 En API-first ou full-stack enrichi, Rails reste l’un des frameworks les plus productifs et polyvalents.<br><br></div><div>N’attendez plus pour repenser vos APIs avec Rails 7.1.<br><br><br></div><div>Happy coding !<br><br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8yMj9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--d25d6d6a355475b4f46f4f7de8f613b8ed301293" content-type="image/gif" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjIsInB1ciI6ImJsb2JfaWQifX0=--339354b961c2fdbc606e57ec2ff36694052f3481/Happy%20Lets%20Go%20GIF%20by%20SpongeBob%20SquarePants.gif" filename="Happy Lets Go GIF by SpongeBob SquarePants.gif" filesize="414066" width="500" height="286" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--gif">
    
    <img class="attachment--preview" src="https://res-3.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/zidekmbrn358pl571xya4prjdtvg">

</figure></action-text-attachment></div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/construire-des-apis-modernes-avec-ruby-on-rails-rest-json-graphql-turbo-plus-encore</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/construire-des-apis-modernes-avec-ruby-on-rails-rest-json-graphql-turbo-plus-encore</guid>
        <pubDate>Sat, 29 Mar 2025 13:13:38 +0100</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
            <category><![CDATA[API]]></category>
            <category><![CDATA[Ruby on Rails]]></category>
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Intermédiaire à Confirmé]]></category>
        
        <!-- Image principale -->
          <enclosure url="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjMsInB1ciI6ImJsb2JfaWQifX0=--0fddba304268de0a85434549e28a8bf386a822a3/pexels-realtoughcandy-11035481.jpg" 
                     type="image/jpeg" 
                     length="543474" />
        
        <!-- Métadonnées supplémentaires -->
          <category><![CDATA[Série: API avec Ruby on Rails]]></category>
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[5 min de lecture]]></category>
      </item>
      <item>
        <title><![CDATA[Comment intégrer des notifications en temps réel dans votre application Rails avec Noticed]]></title>
        
        <!-- Description courte pour compatibilité -->
        <description><![CDATA[Dans le développement d’applications web modernes, les notifications jouent un rôle crucial pour informer l’utilisateur en temps réel de nouveaux é...]]></description>
        
        <!-- Contenu complet pour lecteurs avancés -->
        <content:encoded><![CDATA[<div class="trix-content">
  <div>Dans le développement d’applications web modernes, les notifications jouent un rôle crucial pour informer l’utilisateur en temps réel de nouveaux événements (commentaires, mentions, likes, etc.). Cependant, il arrive que la documentation en français sur ces sujets soit obsolète ou difficile à trouver.<br>Dans cet article, nous allons voir comment intégrer des notifications dans une application Ruby on Rails en utilisant le gem <strong>Noticed </strong>(version 1.6). Nous verrons, étape par étape, la création d’une nouvelle application, l’installation de Noticed, la configuration des notifications via ActionCable et Turbo Streams, et enfin la mise en place d’un système de notifications en temps réel qui s’actualise sans recharger la page.<br><br><br><strong>1. Création d'une nouvelle application Rails avec Tailwind pour le CSS<br></strong><br>
</div><pre>rails new captain_ruby_notifications --skip-active-storage --skip-action-mailbox  --css=tailwind 
cd captain_ruby_notifications</pre><div>
<br><br><strong>2.</strong> <strong>Ajout de nos modèles Post et Comment ainsi que de la gem Devise pour la gestion des utilisateurs<br></strong><br>
</div><div>Ajoutez Devise au Gemfile (ou utilisez bundle add devise) et installez-le :</div><div><br></div><pre>bundle add devise
rails generate devise:install
rails generate devise User</pre><div>
<br>Ensuite, générez les scaffolds pour Post et Comment :</div><div><br></div><pre>rails generate scaffold Post title:string content:text
rails generate scaffold Comment content:text post:references user:references
rails db:migrate</pre><div>
<br>Par la suite, nous allons ajuster notre fichier <strong><em>config/routes.rb</em></strong><em><br><br></em><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8xNT9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--626e4d7ae159d53c26d5217f8b4c7ba71c53f7e1" content-type="image/png" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTUsInB1ciI6ImJsb2JfaWQifX0=--788ee1fbcc9ec5d441a99d86f2ccffc316d852f2/j4jvxawajn5i2xv02vursj858s99.png" filename="j4jvxawajn5i2xv02vursj858s99.png" filesize="122115" width="1464" height="710" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--png">
    
    <img class="attachment--preview" src="https://res-3.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/i1j2gfsqt750a6ww52dc712yjkju">

</figure></action-text-attachment><br><br><strong>3. Ajout du gem Noticed dans le Gemfile<br><br></strong>Ouvrez le fichier Gemfile et ajoutez :</div><pre>gem "noticed", "~&gt; 1.6"</pre><div><br></div><ol>
<li>Puis exécutez :</li>
<li><pre>bundle install</pre></li>
</ol><div>
<br>Cela va nous permettre d'avoir accès à une API unifiée pour livrer des notifications via plusieurs canaux (base de données, ActionCable, email, etc.).</div><div>Noticed simplifie aussi l’implémentation d’actualisations en temps réel (via Turbo Streams et ActionCable).<br><br><br><strong>4. Génération du modèle Notification et&nbsp; de la classe de Notification (Notifier)<br><br></strong>&nbsp;Noticed stocke les notifications dans la base de données. Pour générer le modèle nécessaire, utilisez la commande :</div><pre>rails generate noticed:model Notification
rails generate noticed:notification CommentNotification
rails db:migrate</pre><div>
<br>Nous allons retouver notre fichier qui été généré dans : <strong><em>app/notifications/comment_notification.rb</em></strong><em><br><br></em>&nbsp;Modifiez-le comme suit&nbsp; :</div><pre>class CommentNotification &lt; Noticed::Base
  deliver_by :database
  deliver_by :action_cable, format: :to_action_cable

  # On attend un paramètre :message (chaîne de caractères)
  param :message

  # Après la livraison, nous diffusons la notification via Turbo Streams
  after_deliver :broadcast_notification

  def to_database
    { message: params[:message] }
  end

  def to_action_cable
    { title: "Nouveau commentaire", message: params[:message], id: record.id }
  end

  private

  def broadcast_notification
    recipient.broadcast_prepend_later_to(
      "notifications_#{recipient.id}_dropdown_list",
      target: "notification-dropdown-list",
      partial: "notifications/notification",
      locals: { notification: self.record }
    )

    recipient.broadcast_replace_later_to(
      "notifications_#{recipient.id}_counter",
      target: "notification-counter",
      partial: "notifications/notification_counter",
      locals: { user: recipient }
    )
  end
end</pre><div>
<br><strong><br>5.</strong> <strong>Ajustement des modèles</strong>&nbsp;</div><div>
<br>&nbsp; &nbsp; &nbsp; a. Dans <strong><em>app/models/user.rb</em></strong>, ajoutez l’association pour les notifications :</div><pre>class User &lt; ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy

  # Note : le modèle est généré sous le nom Notification
  has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Notification"
end</pre><div>&nbsp;<br>&nbsp; &nbsp; &nbsp; b.&nbsp; Dans <strong><em>app/models/comment.rb</em></strong>, ajoutez le callback pour déclencher la notification :&nbsp;</div><pre>class Comment &lt; ApplicationRecord
  belongs_to :post
  belongs_to :user

  validates :content, presence: true

  # Après création, déclencher la notification (sauf si l'auteur commente son propre article)
  after_create_commit :notify_post_user

  private

  def notify_post_user
    return if post.user == user

    CommentNotification.with(
      message: "#{user.email} à commenté votre article : #{post.title}"
    ).deliver_later(post.user)
  end
end</pre><div>
<br><br>
</div><ol>
<li><strong>6. Une fois les modèles définis, nous allons maintenant mettre à jour le contrôleur des commentaires (CommentController.rb)</strong></li>
<li><br></li>
<li>&nbsp; Voici un exemple mis à jour pour <strong><em>app/controllers/comments_controller.rb</em></strong> qui gère le streaming via Turbo Streams : &nbsp;</li>
</ol><div>
<br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8xNj9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--8b9e89af2dce65c82a6885b1f4c263bc09f90a2f" content-type="image/png" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTYsInB1ciI6ImJsb2JfaWQifX0=--66f571a95b0b9970efa97961a5e214273786abba/b952i1hmfghnx94c1k826592h2d4.png" filename="b952i1hmfghnx94c1k826592h2d4.png" filesize="520562" width="2280" height="2876" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--png">
    
    <img class="attachment--preview" src="https://res-1.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/rxi4bfq7a2a07wq03tzkzgbp6wnz">

</figure></action-text-attachment><br>Puis créer un fichier Turbo Stream pour la méthode "create".<br><br>
</div><pre>code app/views/comments/create.turbo_stream.erb</pre><div>
<br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8xNz9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--6f8ca756114f00a7a9e2c5226b564f55b0289567" content-type="image/png" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTcsInB1ciI6ImJsb2JfaWQifX0=--7a5fae5949067ff290d1b649dc158ea0634d5a70/gppbpawhtul22h76q4xxhv33qgs0.png" filename="gppbpawhtul22h76q4xxhv33qgs0.png" filesize="100173" width="2128" height="406" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--png">
    
    <img class="attachment--preview" src="https://res-3.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/j5g2xvzqdz89t13vcgiyvqwkrgqc">

</figure></action-text-attachment><br>&nbsp;</div><div>
<strong>7. Mise en place des vues pour les notifications</strong><br><br>&nbsp; &nbsp; &nbsp; a.&nbsp; Créez le fichier <strong><em>app/views/notifications/_notification.html.erb</em></strong>, qui sera le corps de nos notifications :&nbsp;</div><pre>&lt;div id="notification-&lt;%= notification.id %&gt;" class="notification group relative flex flex-col sm:flex-row items-start sm:items-center bg-white hover:bg-gray-50 border border-gray-100 rounded-lg p-4 mb-2 shadow-sm transition-all duration-300 ease-in-out"&gt;
  &lt;div class="flex-grow w-full sm:pr-4"&gt;
    &lt;p class="text-gray-800 text-sm sm:text-base mb-1.5"&gt;
      &lt;%= notification.params[:message] %&gt;
    &lt;/p&gt;
    &lt;p class="text-xs text-gray-500 mb-2"&gt;
      &lt;%= time_ago_in_words(notification.created_at) %&gt;
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;</pre><div>
<br>&nbsp; &nbsp; &nbsp; b.&nbsp; Créez <strong><em>app/views/notifications/_notification_counter.html.erb</em></strong>, le compteur de notifications :</div><pre>&lt;% unread_count = user.notifications.unread.count %&gt;
&lt;% if unread_count.positive? %&gt;
  &lt;span class="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold px-1.5 py-0.5 rounded-full"&gt;
    &lt;%= unread_count %&gt;
  &lt;/span&gt;
&lt;% end %&gt; </pre><div>&nbsp; &nbsp; &nbsp;<br>&nbsp; &nbsp; &nbsp; c.&nbsp; Créez <strong>app/views/notifications/_notification.turbo_stream.erb</strong> , fichier Turo Stream pour streamer en temps réel les notification et le compteur :</div><pre>&lt;%= turbo_stream.prepend "notification-dropdown-list", partial: "notifications/notification", locals: { notification: notification } %&gt;
&lt;%= turbo_stream.replace "notification-counter", partial: "notifications/notification_counter", locals: { user: notification.recipient } %&gt;</pre><div>&nbsp;<br>&nbsp; &nbsp; &nbsp; d.&nbsp; Créez un fichier nommé <strong>_navbar.html.erb</strong> dans le dossier <strong>app/views/layouts/</strong>. Par exemple :</div><pre>&lt;!-- app/views/layouts/_navbar.html.erb --&gt;
&lt;nav class="bg-white shadow"&gt;
  &lt;div class="max-w-7xl mx-auto px-4"&gt;
    &lt;div class="flex justify-between items-center py-4"&gt;
      &lt;!-- Logo et liens principaux --&gt;
      &lt;div class="flex items-center space-x-4"&gt;
        &lt;a href="&lt;%= root_path %&gt;" class="flex items-center text-gray-700"&gt;
          &lt;span class="font-bold text-xl"&gt;Captain Ruby Notifications&lt;/span&gt;
        &lt;/a&gt;
        &lt;div class="hidden md:flex items-center space-x-1"&gt;
          &lt;%= link_to "Articles", posts_path, class: "py-2 px-3 text-gray-700 hover:text-gray-900" %&gt;
          &lt;!-- Ajoutez d'autres liens si nécessaire --&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- Section droite : notifications et authentification --&gt;
      &lt;div class="flex items-center space-x-4"&gt;
        &lt;% if user_signed_in? %&gt;
          &lt;div class="relative group"&gt;
            &lt;div class="cursor-pointer p-2"&gt;
              &lt;!-- Icône de notifications --&gt;
              &lt;svg class="h-6 w-6 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor"&gt;
                &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 
                      d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6 6 0 10-12 0v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1"/&gt;
              &lt;/svg&gt;
              &lt;!-- Compteur de notifications --&gt;
              &lt;%= turbo_stream_from "notifications_#{current_user.id}_counter", id: "notification_counter_stream" %&gt; 
              &lt;div id="notification-counter"&gt;
                &lt;%= render "notifications/notification_counter", user: current_user %&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;!-- Liste des notifications --&gt;
            &lt;%= turbo_stream_from "notifications_#{current_user.id}_dropdown_list", id: "notification_dropdown_stream" %&gt; 
            &lt;div id="notification-dropdown-list" class="absolute right-0 mt-2 w-80 bg-white shadow-lg rounded hidden group-hover:block z-50"&gt;
              &lt;div class="p-4 space-y-2"&gt;
                &lt;% if @notifications.any? %&gt;
                  &lt;%= render @notifications.order(created_at: :desc) %&gt;
                &lt;% else %&gt;
                  &lt;p class="text-gray-500"&gt;Aucune notification pour le moment.&lt;/p&gt;
                &lt;% end %&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;%= link_to "Déconnexion", destroy_user_session_path, method: :delete, class: "py-2 px-3 text-gray-700 hover:text-gray-900" %&gt;
        &lt;% else %&gt;
          &lt;%= link_to "Connexion", new_user_session_path, class: "py-2 px-3 text-gray-700 hover:text-gray-900" %&gt;
          &lt;%= link_to "Inscription", new_user_registration_path, class: "py-2 px-3 bg-yellow-400 text-yellow-900 rounded hover:bg-yellow-300 transition duration-300" %&gt;
        &lt;% end %&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;</pre><div>&nbsp;<br>&nbsp; &nbsp; &nbsp; e.&nbsp; Ouvrez votre fichier <strong><em>app/views/layouts/application.html.erb</em></strong> et insérez-y le rendu du partial de la navbar. Par exemple, juste après l'ouverture de la balise <strong>&lt;body&gt;</strong> :<br><br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8xOD9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--8e8d6acf8966a9c52a4835cb4839714e1b9aec20" content-type="image/png" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTgsInB1ciI6ImJsb2JfaWQifX0=--4baf8f5529269a9ba63b146e6126e51d7fc40b7b/yvxti9n4qcfnmn9tpypwe6ze6dw7.png" filename="yvxti9n4qcfnmn9tpypwe6ze6dw7.png" filesize="222364" width="1448" height="1508" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--png">
    
    <img class="attachment--preview" src="https://res-3.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/o5yf7v2xafj4j8o71ana72r1vbng">

</figure></action-text-attachment><br><br><strong>8.&nbsp; Dans </strong><strong><em>app/controllers/application_controller.rb</em></strong><strong>, ajoutez un before_action pour charger les notifications globalement dans l'environement si l'utilisateur est connecté :</strong><br><br><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8xOT9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--3fa848c7495dd3aef0a0ccff6e87917f23f4792a" content-type="image/png" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTksInB1ciI6ImJsb2JfaWQifX0=--6ec378523ddbab7bc07680f43dcd86d7052cbbc7/nx6t6xejm25chq4iufhklnlfa900.png" filename="nx6t6xejm25chq4iufhklnlfa900.png" filesize="115772" width="1402" height="710" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--png">
    
    <img class="attachment--preview" src="https://res-2.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/399sf58qorsq1hdgxhhdgcf7kk8h">

</figure></action-text-attachment><br><br><strong>9.&nbsp; Vérification et tests<br><br></strong>Démarrez le serveur avec :</div><div><br></div><pre>bin/dev</pre><div>
<br>&nbsp;Accédez à votre application dans le navigateur avec l'url : <a href="http://localhost:3000/"><strong><em>http://localhost:3000</em></strong></a>.&nbsp; Vous devriez voir la navbar en haut de la page avec les liens, l’icône de notification, le compteur et la liste déroulante.<br>Les notifications se diffuseront en temps réel via Turbo Streams lorsque de nouveaux commentaires seront créés. &nbsp;<br><br><br><br><br>🎉Et voilà, vous êtes désormais prêts à créer des notifications en temps réel avec Rails&nbsp;<br><br>happy coding!</div><div><br></div><div><action-text-attachment sgid="eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL3J1Ynlpc3QvQWN0aXZlU3RvcmFnZTo6QmxvYi8xND9leHBpcmVzX2luIiwicHVyIjoiYXR0YWNoYWJsZSJ9fQ==--a7f542d0c7d69124731d2635b2964462b0b09892" content-type="image/gif" url="https://captainruby.fr/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTQsInB1ciI6ImJsb2JfaWQifX0=--08e6f8a989732cb256978fa052ca3d34a89f60e6/Happy%20Lets%20Go%20GIF%20by%20SpongeBob%20SquarePants.gif" filename="Happy Lets Go GIF by SpongeBob SquarePants.gif" filesize="414066" width="500" height="286" previewable="true" presentation="gallery"><figure class="attachment attachment--preview attachment--gif">
    
    <img class="attachment--preview" src="https://res-1.cloudinary.com/dge8yully/image/upload/c_limit,f_auto,h_768,q_auto,w_1024/6qtveccs3yse24ys3ctjkvl5qfre">

</figure></action-text-attachment></div>
</div>
]]></content:encoded>
        
        <link>https://captainruby.fr/posts/comment-integrer-des-notifications-en-temps-reel-dans-votre-application-rails-avec-noticed</link>
        <guid isPermaLink="true">https://captainruby.fr/posts/comment-integrer-des-notifications-en-temps-reel-dans-votre-application-rails-avec-noticed</guid>
        <pubDate>Tue, 25 Feb 2025 05:29:52 +0100</pubDate>
        
        <!-- Auteur avec fallback -->
        <author>francilobbie.lalane@gmail.com (Franci-lobbie LALANE)</author>
        <dc:creator>Franci-lobbie LALANE</dc:creator>
        
        <!-- Catégories/tags -->
            <category><![CDATA[rails]]></category>
            <category><![CDATA[noticed]]></category>
            <category><![CDATA[notification]]></category>
        
        <!-- Niveau de difficulté comme catégorie -->
          <category><![CDATA[Intermédiaire]]></category>
        
        <!-- Image principale -->
          <enclosure url="/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MjAsInB1ciI6ImJsb2JfaWQifX0=--10cc35e1ca31333d28546eaee94b276859ffda28/default_featured_image.jpg" 
                     type="image/jpeg" 
                     length="1918524" />
        
        <!-- Métadonnées supplémentaires -->
        
        <!-- Temps de lecture estimé -->
        <category><![CDATA[6 min de lecture]]></category>
      </item>
  </channel>
</rss>