mcpField
L
layouts-and-rendering

layouts-and-rendering

github.com/thinkoodle開発7187 回インストール8 週間前に更新
リポジトリ

layouts-and-rendering について

Rails 開発でレイアウト、テンプレート、パーシャル、レスポンスの選択と実装を支援するスキルです。パーシャルへの明示的なローカル変数の受け渡しと、Turbo に対応した HTTP ステータスコードを重視します。

主な機能

  1. 01render、redirect_to、head の選択
  2. 02コントローラー別・入れ子レイアウトの設定
  3. 03パーシャルへのローカル変数の明示的な受け渡し
  4. 04コレクションと共有領域のレンダリング
  5. 05Turbo 対応ステータスコードの設定
  6. 06二重レンダリングなどの問題の回避

活用シーン

  1. 01CRUD アクションの成功・エラー応答の実装
  2. 02再利用可能なパーシャルと共有レイアウトの作成
  3. 03Turbo のフォーム送信動作の修正

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thinkoodle/rails-skills --skill layouts-and-rendering

インストール後、Claude Code で/layouts-and-renderingを入力して読み込まれたか確認します。

同梱ファイルreference.md

詳細

Rails のレイアウトとレンダリングのエキスパート

適切なステータスコードで、適切な方法により、適切なものをレンダリングします。

第1のルール:パーシャルではインスタンス変数ではなくローカル変数を使う

# ❌ WRONG — implicit coupling, untestable, will break
<%= render "product" %>
# _product.html.erb uses @product

# ✅ RIGHT — explicit, testable, reusable
<%= render partial: "product", locals: { product: @product } %>
# or shorthand:
<%= render "product", product: @product %>

すべてのパーシャルには、ローカル変数を通じてデータを渡します。例外はありません。 パーシャル内のインスタンス変数は、コントローラーとビューの間に見えない結合を生み、パーシャルを再利用したときに問題を引き起こします。

第2のルール:render と redirect_to の使い分け

この2つは根本的に異なる処理です。ここを間違えるのが、エージェントに最もよくある間違いです。

renderredirect_to
処理内容現在のリクエストでテンプレートをレンダリングしますHTTP 302 を送信し、ブラウザーが新しいリクエストを行います
インスタンス変数利用できます(同じリクエスト)失われます(新しいリクエスト)
ブラウザーの URL変わりません新しい URL に変わります
使用する場面エラーの表示、コンテンツの表示変更処理の成功後
HTTP の往復回数0 回(同じリクエスト)1 回(ブラウザーからサーバーへ再度リクエスト)
def create
  @post = Post.new(post_params)
  if @post.save
    redirect_to @post              # ← Success: redirect (browser gets new URL)
  else
    render :new, status: :unprocessable_entity  # ← Failure: render (keep form data)
  end
end

重要: render :action_name は、そのアクションのコードを実行しません。テンプレートを使うだけです。index.html.erb が @posts を必要としている場合、show から :index をレンダリングしても @posts は設定されません。自分で設定するか、代わりにリダイレクトする必要があります。

このスキルを使う場面

  • render、redirect_to、head のどれを使うか選ぶ
  • レイアウトを設定する(コントローラー単位、アクション単位、条件付き、入れ子)
  • 適切なローカル変数を使ったパーシャルを作成する
  • content_for / yield を使って複数セクションのレイアウトを作成する
  • コレクションを効率よくレンダリングする
  • Turbo/Hotwire のステータスコードを正しく扱う
  • レスポンスをストリーミングする
  • コントローラーから JSON/XML/プレーンテキスト/HTML をレンダリングする

手順

ステップ1:適切なレスポンスの種類を選ぶ
# Full HTML response (most common)
render :show                          # Convention: renders show.html.erb
render "products/show"                # Cross-controller template
render partial: "form", locals: { post: @post }

# Data responses
render json: @product                 # Auto-calls .to_json
render xml: @product                  # Auto-calls .to_xml
render plain: "OK"                    # text/plain, no layout
render html: helpers.tag.strong("Hi") # HTML fragment

# Redirect (after successful mutation)
redirect_to @product                  # 302 by default
redirect_to products_path, status: :see_other  # 303 for Turbo
redirect_back fallback_location: root_path

# Headers only
head :no_content                      # 204, for API delete
head :created, location: photo_url(@photo)
ステップ2:ステータスコードを正しく設定する(Turbo では特に重要)

Turbo Drive(Rails 7 以降)では、ステータスコードによって動作が決まります。

# After failed validation — MUST be 422 for Turbo to replace the page
render :new, status: :unprocessable_entity    # 422

# After successful redirect — MUST be 303 for Turbo
redirect_to @post, status: :see_other         # 303

# Turbo Stream responses
render turbo_stream: turbo_stream.remove(@post)  # 200 OK
シナリオステータス理由
バリデーション失敗、フォームを再レンダリング422 :unprocessable_entityTurbo がページのコンテンツを置き換えます
作成/更新の成功303 :see_other(リダイレクト)Turbo が GET でリダイレクト先にアクセスします
削除の成功303 :see_other(リダイレクト)同じ理由です
API の成功200 :ok または 201 :created標準的な API の規約です
見つからない場合404 :not_found標準です

Turbo を使うアプリで status: :see_other を指定せずに redirect_to を使うと、フォーム送信後に Turbo がリダイレクトに正しく従わないことがあります。

ステップ3:レイアウト
Rails がレイアウトを見つける方法
  1. アクション単位:アクション内で render layout: "special" を使う
  2. コントローラー単位:layout "admin" を宣言する
  3. 規約:PhotosController には app/views/layouts/photos.html.erb
  4. フォールバック:app/views/layouts/application.html.erb
# Per-controller layout
class AdminController < ApplicationController
  layout "admin"
end

# Conditional layout
class ProductsController < ApplicationController
  layout "product", except: [:index, :rss]
end

# Runtime layout selection
class ProductsController < ApplicationController
  layout :choose_layout

  private

  def choose_layout
    current_user&.admin? ? "admin" : "application"
  end
end

# Per-action override
def special
  render layout: "minimal"
end

# No layout at all
def api_endpoint
  render json: @data, layout: false
end
レイアウトの継承

レイアウトはコントローラーの階層に沿って引き継がれます。

class ApplicationController < ActionController::Base
  layout "main"       # All controllers use "main"
end

class ArticlesController < ApplicationController
  # Inherits "main" layout
end

class SpecialArticlesController < ArticlesController
  layout "special"    # Overrides to "special"
end

class ApiController < ApplicationController
  layout false        # No layout at all
end
入れ子のレイアウト(サブテンプレート)

content_for と render template: を使って、親レイアウトを拡張します。

<%# app/views/layouts/admin.html.erb — extends application layout %>
<% content_for :head do %>
  <%= stylesheet_link_tag "admin" %>
<% end %>

<% content_for :content do %>
  <div class="admin-sidebar"><%= yield :sidebar %></div>
  <div class="admin-main"><%= yield %></div>
<% end %>

<%= render template: "layouts/application" %>

これに対応するには、アプリケーションのレイアウトを次のように設定します。

<%# app/views/layouts/application.html.erb %>
<html>
<head><%= yield :head %></head>
<body>
  <%= content_for?(:content) ? yield(:content) : yield %>
</body>
</html>
ステップ4:パーシャル — 必ずローカル変数を使う
基本的なパーシャルのレンダリング
# Explicit (preferred when passing locals)
<%= render partial: "form", locals: { post: @post } %>

# Shorthand (works for simple cases)
<%= render "form", post: @post %>

# Model shorthand — renders _post.html.erb with local `post`
<%= render @post %>

# Cross-directory partial
<%= render "shared/navbar", current_user: @user %>
パーシャルの契約

すべてのパーシャルでは、期待するローカル変数を先頭に記述します。

<%# app/views/posts/_post.html.erb %>
<%# locals: (post:, show_actions: true) %>
<article>
  <h2><%= post.title %></h2>
  <p><%= post.body %></p>
  <% if show_actions %>
    <%= link_to "Edit", edit_post_path(post) %>
  <% end %>
</article>

マジックコメント <%# locals: (post:, show_actions: true) %>(Rails 7.1 以降)には、次の2つの役割があります。

  1. 期待するローカル変数を記述します
  2. 必須のローカル変数がない場合にエラーを発生させます
オプションのローカル変数(Rails 7.1 より前)
<%# Check with local_assigns for optional params %>
<% if local_assigns[:full] %>
  <%= simple_format article.body %>
<% else %>
  <%= truncate article.body %>
<% end %>
ステップ5:コレクションのレンダリング

リストには必ずコレクションレンダリングを使います。 レンダリングの呼び出しが1回で済むため高速で、コードもすっきりします。

# ❌ SLOW — N render calls
<% @products.each do |product| %>
  <%= render partial: "product", locals: { product: product } %>
<% end %>

# ✅ FAST — single render call, Rails optimizes internally
<%= render partial: "product", collection: @products %>

# ✅ FASTEST shorthand — Rails infers partial name from model
<%= render @products %>

# Empty collection handling
<%= render(@products) || "No products yet." %>
コレクションの機能
# Custom local variable name
<%= render partial: "product", collection: @products, as: :item %>

# Counter variable (0-indexed) — available as product_counter
# Inside _product.html.erb: product_counter gives 0, 1, 2...

# Spacer template — rendered between items
<%= render partial: @products, spacer_template: "product_divider" %>

# Extra locals passed to every item
<%= render partial: "product", collection: @products,
           locals: { show_price: true } %>

# Layout for each item
<%= render partial: "product", collection: @products, layout: "card" %>
異種コレクション
# Rails picks the right partial based on model class
<%= render [customer1, employee1, customer2] %>
# Renders customers/_customer.html.erb and employees/_employee.html.erb
ステップ6:content_for と yield

content_for を使って、レイアウト内の名前付き領域にコンテンツを挿入します。

<%# Layout: app/views/layouts/application.html.erb %>
<html>
<head>
  <title><%= yield :title %></title>
  <%= yield :head %>
</head>
<body>
  <%= yield :breadcrumbs %>
  <%= yield %>  <%# Main content (unnamed yield) %>
</body>
</html>

<%# View: app/views/posts/show.html.erb %>
<% content_for :title, @post.title %>

<% content_for :head do %>
  <%= tag.meta name: "description", content: @post.excerpt %>
<% end %>

<% content_for :breadcrumbs do %>
  <nav>Posts > <%= @post.title %></nav>
<% end %>

<article>
  <h1><%= @post.title %></h1>
  <%= simple_format @post.body %>
</article>
content_for? — 条件付きセクション
<%# Only render sidebar wrapper if content exists %>
<% if content_for?(:sidebar) %>
  <aside><%= yield :sidebar %></aside>
<% end %>
provide と content_for の使い分け
provide :title, "My Page"       # Sets once, stops looking (streaming-friendly)
content_for :title, "My Page"   # Appends, can be called multiple times

単一の値(ページタイトル)には provide を使います。蓄積するコンテンツ(複数のスクリプトタグ)には content_for を使います。

ステップ 7:二重レンダリングエラーを避ける

Rails では、render または redirect を 2 回実行すると AbstractController::DoubleRenderError が発生します。

# ❌ BUG — both render calls execute
def show
  @book = Book.find(params[:id])
  if @book.special?
    render :special_show
  end
  render :regular_show  # Always runs!
end

# ✅ FIX — return after render
def show
  @book = Book.find(params[:id])
  if @book.special?
    return render :special_show
  end
  render :regular_show
end

# ✅ ALSO FINE — implicit render for else case
def show
  @book = Book.find(params[:id])
  render :special_show if @book.special?
  # Implicit render of :show if special? is false
end

条件付きレンダリングには、return render パターンが最もすっきりしています。

ステップ 8:テンプレートの継承

コントローラーは、親コントローラーからテンプレートの検索方法を継承します。

# Lookup order for Admin::ProductsController#index:
# 1. app/views/admin/products/index.html.erb
# 2. app/views/admin/index.html.erb
# 3. app/views/application/index.html.erb

そのため、共通のパーシャルには app/views/application/ が最適です。

<%# app/views/application/_empty_list.html.erb %>
<p>No items yet.</p>

<%# Usable from any controller's view: %>
<%= render(@products) || render("empty_list") %>

クイックリファレンス

render チートシート
# Templates
render :edit                          # Same controller template
render "edit"                         # Same (string)
render "products/show"                # Other controller template
render template: "products/show"      # Explicit

# Data
render json: @product                 # JSON
render xml: @product                  # XML
render plain: "OK"                    # Plain text
render html: "<b>Hi</b>".html_safe    # HTML fragment
render body: "raw"                    # Raw body, no content type
render js: "alert('hi')"             # JavaScript

# Files
render file: Rails.root.join("public/404.html"), layout: false
render inline: "<%= 1 + 1 %>"        # Don't use this

# Options (combinable)
render :edit, status: :unprocessable_entity
render :show, layout: "minimal"
render :show, layout: false
render :show, content_type: "application/rss"
render :show, formats: [:json]
render :show, variants: [:mobile]
redirect_to チートシート
redirect_to @post                            # record → show path
redirect_to posts_path                       # named route
redirect_to "https://example.com"            # URL
redirect_to action: :index                   # hash
redirect_back fallback_location: root_path   # back button

# With status (important for Turbo!)
redirect_to @post, status: :see_other        # 303
redirect_to posts_path, status: 301          # permanent

# With flash
redirect_to @post, notice: "Created!"
redirect_to @post, alert: "Problem!"
標準的な CRUD コントローラーパターン(Turbo 対応)
class PostsController < ApplicationController
  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to @post, notice: "Created!", status: :see_other
    else
      render :new, status: :unprocessable_entity
    end
  end

  def update
    @post = Post.find(params[:id])
    if @post.update(post_params)
      redirect_to @post, notice: "Updated!", status: :see_other
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def destroy
    @post = Post.find(params[:id])
    @post.destroy!
    redirect_to posts_path, notice: "Deleted!", status: :see_other
  end
end

アンチパターン

  1. パーシャル内のインスタンス変数 — 必ずローカル変数を明示的に渡してください
  2. render を redirect_to と同じだと考える — render は URL を変更せず、アクションも再実行しません
  3. status: :unprocessable_entity の指定漏れ — 422 を返さないと、Turbo はページを置き換えません
  4. status: :see_other の指定漏れ — POST/PUT/DELETE の後、Turbo がリダイレクトに従わない場合があります
  5. render inline: — MVC の原則に反します。テンプレートを使用してください
  6. return なしの二重レンダリング — return render :action パターンを使用してください
  7. フォームエラーに redirect_to を使用する — リダイレクトすると @record.errors が失われるため、代わりに render を使用してください
  8. redirect_back で fallback_location の指定を忘れる — referer がない場合、エラーが発生します
  9. ユーザー入力を使った render file: — パストラバーサルの脆弱性につながります
  10. provide で十分な場合に content_for を使用する — provide はストリーミングに適しています

詳しいレンダリングパターン、レイアウトの例、エッジケースについては、このスキルディレクトリの reference.md を参照してください。

コメント