Aujourd'hui, on te montre comment on passe d'un langage interprété (Ruby) à du HTML côté client en passant par un serveur web minimal. On va rester en Ruby “pur” et bâtir petit à petit, jusqu'à un mini-projet façon mini‑rails.
Objectifs:
utiliser Rack + Puma dans tous les exemples
générer du HTML de 4 façons: string, ERB, Nokogiri::Builder, Phlex
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.
1) HTML en string avec Rack et Puma
On créer le fichier app.rb
require "rack"
require "rack/handler/puma"
app = Proc.new do |env|
html = <<~HTML
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Hello monde!</title></head>
<body>
<h1>Super Ruby</h1>
<p>Du HTML juste avec du Ruby, vraiment ?</p>
</body>
</html>
HTML
[200, { "Content-Type" => "text/html; charset=utf-8" }, [html]]
end
Rack::Handler::Puma.run app, Port: 9292
Lance ruby app.rb, puis visite http://localhost:9292.
Comprendre les briques
app = Proc.new { |env| ... }: une appli Rack est un objet appelable qui reçoit env (un Hash avec la requête: méthode, chemin, headers...) et retourne un triplet [status, headers, body].
200: c'est le code HTTP OK. Comme par exemple (404: not found, 302: redirect, 500: server error).
{"Content-Type" => "text/html; charset=utf-8"}: indique le type de contenu au navigateur. Pour du JSON: application/json.
body: doit être énumérable (Array de strings, généralement). Ici [[html]].
Résumé: on construit le HTML, puis on renvoie [status, headers, body] dans cet ordre.
2) ERB: un template réutilisable
On créer le fichier index.erb
<!doctype html>
<html>
<head><meta charset="utf-8"><title><%= title %></title></head>
<body>
<h1><%= title %></h1>
<ul>
<% items.each do |item| %>
<li><%= item %></li>
<% end %>
</ul>
</body>
</html>
Puis app_erb.rb
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" => "text/html; charset=utf-8" }, [html]]
end
Rack::Handler::Puma.run app, Port: 9292
C'est quoi un objet ERB ?
ERB lit un fichier HTML avec <% %> (Ruby) et <%= %> (Ruby qui affiche).
ERB.new(...) compile le template.
result_with_hash(...) évalue le template avec des variables.
Pour comparer les moteurs, on a un billet: HAML vs ERB : quel moteur de template choisir pour vos vues Rails ? sur Captain Ruby.
3) Nokogiri::HTML::Builder: générer le HTML en Ruby
A la racine: app_nokogiri.rb
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" => "text/html; charset=utf-8" }, [builder.to_html]]
end
Rack::Handler::Puma.run app, Port: 9292
Pourquoi c'est pratique ? Boucles/conditions naturelles, markup toujours valide, et post‑traitement DOM facile.
4) Phlex en mode mini‑rails : assets, routing, base de données
On passe à un mini projet structuré pour se rapprocher d'une app web réelle.
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 < ActiveRecord::Base; end
4.2 Vues Phlex
views/layout.rb
require "phlex"
class Layout < Phlex::HTML
def initialize(title: "Mini-Rails"); @title = title; 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
views/posts_index.rb
require "phlex"
require_relative "layout"
class PostsIndex < Phlex::HTML
def initialize(posts:)
@posts = posts
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 |post|
li do
a(href: "/posts/#{post.id}") { post.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
views/posts_show.rb
require "phlex"
require_relative "layout"
class PostsShow < Phlex::HTML
def initialize(post:); @post = post; 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
4.3 Une page d'acceuil simple views/home_page.rb
require "phlex"
require_relative "layout"
class HomePage < 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
4.4 App + routing + assets
app.rb
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" # ← ajoute ceci
PUBLIC_DIR = File.expand_path("./public", __dir__)
app = Proc.new do |env|
req = Rack::Request.new(env)
# Assets statiques
if req.path.start_with?("/style.css") || req.path.start_with?("/app.js")
next Rack::Files.new(PUBLIC_DIR).call(env)
end
case [req.request_method, req.path]
when ["GET", "/"]
html = HomePage.new.call # ← n’appelle plus Layout directement ici
[200, { "Content-Type" => "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" => "text/html; charset=utf-8" }, [html]]
when ["POST", "/posts"]
title = req.params["title"]&.strip
body = req.params["body"]&.strip
if title.to_s.empty? || body.to_s.empty?
next [422, { "Content-Type" => "text/html; charset=utf-8" }, ["Titre et contenu requis."]]
end
post = Post.create!(title: title, body: body)
[302, { "Location" => "/posts/#{post.id}" }, []]
else
if req.get? && req.path =~ %r{^/posts/(\d+)$}
id = req.path.match(%r{/posts/(\d+)$})[1]
post = Post.find_by(id: id)
next [404, { "Content-Type" => "text/html" }, ["Not found"]] unless post
html = PostsShow.new(post: post).call
[200, { "Content-Type" => "text/html; charset=utf-8" }, [html]]
else
[404, { "Content-Type" => "text/plain" }, ["Not found"]]
end
end
end
Rack::Handler::Puma.run app, Port: 9292
Lance bundle exec ruby app.rb et visite http://localhost:9292. Tu as maintenant:
des assets servis depuis public/
un routing simple en fonction de la méthode et du chemin
un mini-blog avec SQLite + Active Record (création et lecture)
Conclusion
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:
string brute quand on veut juste tester un truc vite fait,
ERB pour rester proche du HTML,
Nokogiri pour générer du markup avec de la logique métier,
Phlex quand on veut des vues composables, testables, et une structure propre.
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.
Et maintenant ? On te laisse deux pistes simples pour continuer:
Ajouter l’édition et la suppression des articles (GET/POST /posts/:id/edit, DELETE /posts/:id).
Introduire des layouts/composants Phlex réutilisables (footer, flash messages), et un config.ru pour lancer avec rackup.
Si tu tentes l’une des variantes, dis-nous ce que tu as bricolé. On est chauds pour en faire un suivi.