第13章|今さら学ぶ「ビュー詳解(ERB / フォーム / ヘルパー)」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- ERB(Embedded Ruby)— HTMLにRubyを埋め込む仕組み
-
<%= %>と<% %>の違い -
form_withの使い方 — new と edit で同じフォームを使い回す -
link_to/button_toの使い分け - Partial(部分テンプレート)— 見た目の部品を使い回す
- Helper メソッド — 表示のための裏方の仕事
- OGP・メタタグ — SNSシェア時のプレビューと
<head>管理
🏠 たとえ話で掴む「ビュー」
ビューの役割は、第9章で「レストランのメニュー表・食器」にたとえました。今回はもう少し具体的に、 手紙のテンプレート で考えます。
年賀状を出すとき、宛名や挨拶文のテンプレートを使います。テンプレートに「{名前}様」と書いておけば、実際に印刷するときに「田中様」「鈴木様」と差し替えられます。
ERBはまさにこれと同じです。HTMLの中に「ここにRubyの値を入れてね」という目印を埋め込んでおき、リクエスト時に実際のデータで差し替えます。
| 手紙テンプレート | ERBテンプレート |
|---|---|
| テンプレートを用意する |
.html.erb ファイルを作る |
{名前}様 と差し込み欄を作る |
<%= user.name %> と書く |
| 印刷時に実際の名前を入れる | リクエスト時にRubyが値を埋め込む |
| 共通の挨拶文は使い回す | Partial(部分テンプレート)で使い回す |
ビューとは何か — 技術的な定義
MVCにおけるビューの役割
ビュー(View) は、MVCアーキテクチャの中で ユーザーに見せる画面を組み立てる層 です。
コントローラから受け取ったデータ(インスタンス変数)を使って、HTMLを動的に生成します。ビュー自身はデータの取得や加工を行わず、 渡されたデータの表示 に徹するのが原則です。
Railsのビューは Action View というコンポーネントが担当しています。ERBテンプレートの解釈、フォームヘルパーの提供、Partialの管理など、表示に関する機能がまとまっています。
テンプレートエンジンとしてのERB
ERB(Embedded Ruby) はRuby標準のテンプレートエンジンで、任意のテキストファイルにRubyコードを埋め込む仕組みです。Railsではこれを使って「HTMLの中にRubyを書く」ことで動的なページを生成します。ファイルの拡張子は .html.erb です。
📝 ERB — HTMLの中にRubyを埋め込む
<%= %> と <% %> の違い
ERBには2種類のタグがあります。
<%# ① <%= %> — 結果をHTMLに出力する(=がついている) %>
<h1><%= @article.title %></h1>
<%# → <h1>Railsのルーティングを完全理解する</h1> %>
<%# ② <% %> — 実行するだけで出力しない(=がない) %>
<% if @article.published? %>
<span class="text-green-600">公開中</span>
<% else %>
<span class="text-gray-500">下書き</span>
<% end %>
| タグ | 出力 | 使いどころ |
|---|---|---|
<%= %> |
あり | データを画面に表示するとき |
<% %> |
なし | if文、ループなどの制御構造 |
よくある間違いは、each に = をつけてしまうことです。
<%# ❌ 間違い — each自体の戻り値が出力されてしまう %>
<%= @articles.each do |article| %>
<%# ✅ 正しい — eachは実行するだけ %>
<% @articles.each do |article| %>
📋 form_with — 申請書のひな形を自動生成する
基本の使い方
form_with は、Railsのフォームヘルパーです。モデルに対応するフォームを自動生成してくれます。
<%= form_with(model: @article, class: "space-y-4") do |f| %>
<div>
<%= f.label :title, "タイトル" %>
<%= f.text_field :title, class: "w-full border rounded p-2" %>
</div>
<div>
<%= f.label :body, "本文" %>
<%= f.text_area :body, rows: 10, class: "w-full border rounded p-2" %>
</div>
<%= f.submit class: "bg-blue-600 text-white px-6 py-2 rounded" %>
<% end %>
new と edit で同じフォームを使い回す仕組み
form_with(model: @article) の賢いところは、 @article が新規か既存かを自動で判断してくれる 点です。
# new アクション — @article が新規(未保存)
@article = Article.new
# → form_with は POST /articles を生成
# edit アクション — @article が既存(保存済み)
@article = Article.find(params[:id])
# → form_with は PATCH /articles/1 を生成
フォームのHTML自体は同じなのに、送信先やHTTPメソッドが自動で切り替わります。だから new と edit で 同じPartialを使い回せます。
よく使うフォームヘルパー
<%= f.text_field :title %> <%# 1行のテキスト入力 %>
<%= f.text_area :body, rows: 10 %> <%# 複数行のテキスト入力 %>
<%= f.email_field :email %> <%# メールアドレス入力 %>
<%= f.password_field :password %> <%# パスワード入力(伏せ字) %>
<%= f.check_box :published %> <%# チェックボックス %>
<%= f.select :status, ["draft", "published"] %> <%# プルダウン %>
<%= f.file_field :image %> <%# ファイルアップロード %>
<%= f.hidden_field :user_id %> <%# 隠しフィールド %>
<%= f.submit "投稿する" %> <%# 送信ボタン %>
🔗 link_to / button_to — ページ間の道しるべとアクションボタン
link_to — リンクを生成する
<%# 基本の使い方 %>
<%= link_to "記事一覧", articles_path %>
<%# → <a href="/articles">記事一覧</a> %>
<%# CSSクラスをつける %>
<%= link_to "記事を見る", article_path(@article),
class: "text-blue-600 hover:underline" %>
<%# ブロックで中身を自由に書く %>
<%= link_to article_path(@article) do %>
<div class="p-4 border rounded">
<h2><%= @article.title %></h2>
<p><%= @article.user.display_name %></p>
</div>
<% end %>
button_to — フォーム送信ボタンを生成する
<%# DELETEリクエストを送るボタン %>
<%= button_to "削除", article_path(@article),
method: :delete,
class: "text-red-600",
data: { turbo_confirm: "本当に削除しますか?" } %>
link_to と button_to の使い分け
link_to |
button_to |
|
|---|---|---|
| 生成するHTML |
<a> タグ |
<form> + <button> タグ |
| デフォルトのメソッド | GET | POST |
| 使いどころ | ページ遷移(閲覧系) | データ変更(作成・削除系) |
| 例 | 記事詳細へのリンク | 削除ボタン、いいねボタン |
基本ルールは GETなら link_to、POST / PATCH / DELETE なら button_to です。Turboとの連携(Turbo FramesやTurbo Streams)は(→ 第19章で詳しく扱います)。
🧩 Partial(部分テンプレート)— 見た目の部品を使い回す
Partial は、ビューの一部を別ファイルに切り出して使い回す仕組みです。UIの「部品化」にあたります。
基本の使い方
<%# app/views/articles/index.html.erb %>
<h1>記事一覧</h1>
<% @articles.each do |article| %>
<%= render "article", article: article %>
<%# ↑ _article.html.erb を呼び出す %>
<% end %>
<%# app/views/articles/_article.html.erb(先頭に _ がつくのがPartial) %>
<div class="p-4 border rounded mb-4">
<h2 class="text-xl font-bold">
<%= link_to article.title, article_path(article) %>
</h2>
<p class="text-gray-500">
by <%= article.user.display_name %> · <%= article.created_at.strftime("%Y/%m/%d") %>
</p>
<div class="flex gap-2 mt-2">
<span>❤️ <%= article.likes.count %></span>
<span>💬 <%= article.comments.count %></span>
</div>
</div>
コレクションの省略記法
each + render の組み合わせは、もっと短く書けます。
<%# 上のeach + render と同じ結果 %>
<%= render partial: "article", collection: @articles %>
<%# さらに省略(Railsが自動でPartial名を推測) %>
<%= render @articles %>
フォームをPartial化する
new と edit で同じフォームを使い回す定番パターンです。
<%# app/views/articles/new.html.erb %>
<h1>記事を書く</h1>
<%= render "form", article: @article %>
<%# app/views/articles/edit.html.erb %>
<h1>記事を編集</h1>
<%= render "form", article: @article %>
<%# app/views/articles/_form.html.erb — 共通フォーム %>
<%= form_with(model: article, class: "space-y-4") do |f| %>
<% if article.errors.any? %>
<div class="bg-red-50 text-red-700 p-4 rounded">
<ul>
<% article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :title, "タイトル", class: "block font-bold mb-1" %>
<%= f.text_field :title, class: "w-full border rounded p-2" %>
</div>
<div>
<%= f.label :body, "本文", class: "block font-bold mb-1" %>
<%= f.text_area :body, rows: 10, class: "w-full border rounded p-2" %>
</div>
<%= f.submit class: "bg-blue-600 text-white px-6 py-2 rounded" %>
<% end %>
🔨 Helper メソッド — 表示のための裏方
ヘルパー は、ビューで使う便利メソッドをまとめる場所です。ビューに複雑なRubyロジックを書きたくないとき、ヘルパーに切り出します。
# app/helpers/articles_helper.rb
module ArticlesHelper
# Markdownを安全にHTMLに変換する
def markdown(text)
renderer = Redcarpet::Render::HTML.new(
hard_wrap: true,
filter_html: true # Markdownで生成されたもの以外の生HTMLタグをすべて削除する
)
extensions = {
fenced_code_blocks: true, # ```で囲んだコードブロック
autolink: true, # URLを自動リンク化
tables: true # テーブル記法
}
Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
end
# 記事のステータスをバッジ表示する
def status_badge(article)
if article.published?
tag.span("公開中", class: "bg-green-100 text-green-700 px-2 py-1 rounded text-sm")
else
tag.span("下書き", class: "bg-gray-100 text-gray-600 px-2 py-1 rounded text-sm")
end
end
end
<%# ビューではヘルパーを呼ぶだけ %>
<div class="prose">
<%= markdown(@article.body) %>
</div>
<%= status_badge(@article) %>
ヘルパーに切り出す基準は、 「ビューの中にif文やループが増えてきたら」 です。ビューはなるべくHTMLに近い見た目を保ち、ロジックはヘルパーに任せる方が保守しやすくなります。html_safeの使用に関するXSSリスクについては(→ 第20章で詳しく扱います)。
🏷️ OGP・メタタグ — <head> タグを管理する
OGP(Open Graph Protocol) は、WebページがSNSでシェアされたときのプレビュー表示を制御するメタタグの規格です。FacebookやX(旧Twitter)が採用しています。
KnowledgeNoteの記事をSNSでシェアしたとき、タイトル・説明・サムネイル画像が綺麗に表示されるのはOGP設定のおかげです。
<!-- SNSシェア時のプレビューを制御するOGPタグ -->
<meta property="og:title" content="Railsのルーティングを完全理解する">
<meta property="og:description" content="resources メソッドが自動生成する7つのルートを...">
<meta property="og:image" content="https://example.com/ogp/article_1.png">
<meta property="og:type" content="article">
<meta property="og:url" content="https://example.com/articles/1">
<!-- X(旧Twitter)カード(大きな画像プレビュー) -->
<meta name="twitter:card" content="summary_large_image">
<!-- 検索エンジン向けの説明文 -->
<meta name="description" content="Railsのルーティングを完全理解する">
content_for / yield でページごとに変える
Railsでは content_for ヘルパーを使って、ページごとに <head> の内容を差し込めます。
<%# app/views/layouts/application.html.erb — レイアウトテンプレート %>
<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title).presence || "KnowledgeNote" %></title>
<meta name="description"
content="<%= yield(:description).presence || "学習記録を共有するプラットフォーム" %>">
<%# OGPタグ %>
<meta property="og:title"
content="<%= yield(:og_title).presence || "KnowledgeNote" %>">
<meta property="og:description"
content="<%= yield(:og_description).presence || "学習記録を共有するプラットフォーム" %>">
<meta property="og:image"
content="<%= yield(:og_image).presence || image_url("default_ogp.png") %>">
<meta property="og:type"
content="<%= yield(:og_type).presence || "website" %>">
<meta property="og:url" content="<%= request.original_url %>">
<meta name="twitter:card" content="summary_large_image">
<%= yield :head %>
<%= stylesheet_link_tag "application" %>
</head>
<body>
<%= yield %>
</body>
</html>
<%# app/views/articles/show.html.erb — ページ固有のメタ情報を定義する %>
<% content_for :title, @article.title %>
<% content_for :description, @article.body.truncate(120) %>
<% content_for :og_title, @article.title %>
<% content_for :og_description, @article.body.truncate(120) %>
<% content_for :og_type, "article" %>
<article>
<h1><%= @article.title %></h1>
...
</article>
| ヘルパー | 役割 |
|---|---|
content_for :key |
特定のキーに内容を蓄積する |
yield(:key) |
レイアウト側で蓄積した内容を出力する |
.presence |
値がnil/空文字ならnilを返す(|| でデフォルト値を設定するため) |
ヘルパーにまとめる
OGP関連のロジックが増えてきたら、ヘルパーに切り出すと整理できます。
# app/helpers/meta_tags_helper.rb
module MetaTagsHelper
def page_title(title = nil)
title.present? ? "#{title} | KnowledgeNote" : "KnowledgeNote"
end
def og_image_url(article = nil)
if article&.thumbnail&.attached?
url_for(article.thumbnail)
else
image_url("default_ogp.png")
end
end
end
<%# app/views/layouts/application.html.erb %>
<title><%= page_title(yield(:title)) %></title>
<meta property="og:image" content="<%= yield(:og_image).presence || image_url("default_ogp.png") %>">
<%# app/views/articles/show.html.erb — og_imageはヘルパーで生成してcontent_forで渡す %>
<% content_for :og_image, og_image_url(@article) %>
🛠️ KnowledgeNoteでの具体例
記事詳細ページの全体像です。ここまで学んだ全ての要素が使われています。
<%# app/views/articles/show.html.erb %>
<article class="max-w-3xl mx-auto p-6">
<%# 記事ヘッダー %>
<h1 class="text-3xl font-bold text-gray-800 mb-4">
<%= @article.title %>
</h1>
<div class="flex items-center gap-4 text-gray-500 mb-6">
<%= link_to @article.user.display_name, user_path(@article.user),
class: "hover:text-blue-600" %>
<time><%= @article.created_at.strftime("%Y年%m月%d日") %></time>
<%= status_badge(@article) %>
</div>
<%# 記事本文(Markdown → HTML変換) %>
<div class="prose max-w-none mb-8">
<%= markdown(@article.body) %>
</div>
<%# タグ一覧 %>
<div class="flex gap-2 mb-8">
<% @article.tags.each do |tag| %>
<%= link_to tag.name, tag_path(tag),
class: "bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm" %>
<% end %>
</div>
<%# いいねボタン %>
<div class="mb-8">
<%= render "likes/button", likeable: @article %>
</div>
<%# コメント一覧 — コレクションのPartial %>
<h2 class="text-xl font-bold mb-4">コメント(<%= @article.comments.count %>件)</h2>
<%= render @article.comments.top_level.includes(:user, :replies) %>
<%# コメント投稿フォーム %>
<% if current_user %>
<%= render "comments/form", article: @article, comment: Comment.new %>
<% end %>
</article>
💼 面接で聞かれたら?
Q:Partialとは何ですか?どういう場面で使いますか?
「Partialはビューの一部を別ファイルに切り出して再利用する仕組みです。ファイル名の先頭にアンダースコアをつけ、
renderで呼び出します。記事一覧の各行、new/editで共通のフォーム、ヘッダー・フッターなどの共通UIパーツに使います。同じHTMLを複数箇所に書かずに済むので、DRY原則を実現できます。」深掘りされたら:
- 「
<%= %>と<% %>の違いは?」→<%= %>はRubyの評価結果をHTMLに出力する。<% %>は実行するだけで出力しない。if文やeachでは<% %>を使い、変数の値を表示するときは<%= %>を使う。- 「form_withはnewとeditでどう使い分ける?」→ 使い分けない。
form_with(model: @article)がモデルの状態(新規/既存)を自動判断して、送信先(POST/PATCH)を切り替えてくれる。
🔗 もっと深く知りたい人へ(1次情報リンク)
- Rails ガイド:レイアウトとレンダリング — Partial・レイアウト・renderの全オプション
- Rails ガイド:Action View フォームヘルパー — form_with の全パターン
- Rails ガイド:Action View ヘルパー — link_to, button_to, tag 等の全メソッド
まとめ
- ✅ ERBはHTMLにRubyを埋め込むテンプレート。
<%= %>は出力あり、<% %>は出力なし - ✅
form_with(model:)は新規/既存を自動判断。同じフォームをnew/editで使い回せる - ✅ GETなら
link_to、POST/PATCH/DELETEならbutton_toを使う - ✅ Partialでビューの部品を切り出して再利用。ファイル名は
_で始まる - ✅ ヘルパーで表示ロジックをビューから切り出し、ビューをHTMLに近い形に保つ
- ✅
content_for/yieldでページごとに<head>のOGPタグを差し込む。SNSシェア時のプレビューが変わる
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに