action-text について
Rails 開発者向けに、Action Text のリッチテキスト項目、エディター、添付ファイル、安全な表示、テストの実装方法を案内するスキルです。Rails のバージョンに応じた Lexxy と Trix の使い分けや、N+1 クエリを避けるプリロードも説明します。
主な機能
- 01Action Text の導入と設定
- 02Lexxy または Trix による編集
- 03安全な表示とプレーンテキスト抽出
- 04Active Storage による添付ファイル管理
- 05カスタムオブジェクトの埋め込み
- 06N+1 クエリ回避のためのプリロード
活用シーン
- 01Rails モデルにリッチテキスト項目を追加
- 02フォームにリッチテキストエディターを導入
- 03記事内に画像やファイルを表示
- 04一覧ページのリッチテキストクエリを最適化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill action-textインストール後、Claude Code で/action-textを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/attachables.mdeditors.mdsecurity.mdstyling.mdtesting.md詳細
Rails Action Text エキスパート
Lexxy エディター(Rails 8.1 以降)または Trix(Rails 6~8.0)を使用して、Rails アプリケーションでリッチテキスト編集を実装します。
主な概念
Action Text は、ポリモーフィック関連付けを介してリッチテキストを別の action_text_rich_texts テーブルに保存します。モデルのテーブルには保存されません。サニタイズ、レンダリング、添付ファイル(Active Storage 経由)、埋め込みオブジェクト(Signed GlobalIDs 経由)を処理します。
エディターの歴史:
- Trix — Action Text のオリジナルエディター(Rails 6 以降)です。現在も動作し、サポートされています。
- Lexxy — Basecamp による最新の代替エディター(Rails 8.1 以降)です。ダークモードの改善、CSS カスタムプロパティ、優れた UX を備えています。新しいプロジェクトでは Lexxy を使用してください。
このスキルを使う場面
- モデルにリッチテキストフィールドを追加する
- 新規または既存の Rails アプリで Action Text をセットアップする
- Lexxy または Trix エディターの外観を設定する
- リッチテキストコンテンツを安全にレンダリングする
- リッチテキスト内の画像/ファイル添付を処理する
- カスタム添付可能オブジェクトを作成する(ユーザー、製品などを埋め込む)
- リッチテキストの N+1 クエリを修正する
- エディターとレンダリングされたコンテンツのスタイルを設定する
- リッチテキスト機能をテストする
手順
ステップ 1:Action Text がインストールされているか確認する
# Check for Action Text tables
bin/rails runner "puts ActiveRecord::Base.connection.table_exists?('action_text_rich_texts')"
# Check for Action Text config files
ls app/views/layouts/action_text/contents/_content.html.erb 2>/dev/null
ls app/views/active_storage/blobs/_blob.html.erb 2>/dev/null
# Check Gemfile for editor
grep -E "trix|lexxy" Gemfile
インストールされていない場合は、先にインストールを実行します(ステップ 2)。すでにインストールされている場合は、ステップ 3 に進みます。
ステップ 2:Action Text をインストールする
bin/rails action_text:install
bin/rails db:migrate
次のものが作成されます:
action_text_rich_textsテーブル用のマイグレーション(不足している場合は Active Storage のテーブルも作成)- エディター用の JavaScript インポート
app/views/layouts/action_text/contents/_content.html.erb— コンテンツラッパーの部分テンプレートapp/views/active_storage/blobs/_blob.html.erb— 添付ファイルをレンダリングする部分テンプレートapp/assets/stylesheets/actiontext.css— デフォルトのスタイル
Lexxy(Rails 8.1 以降)の場合は、次も追加します:
# Gemfile
gem "lexxy"
bundle install
重要:Lexxy のスタイルシートの読み込み順序:
<%# app/views/layouts/application.html.erb %>
<%# Lexxy FIRST, then app styles (so your overrides win) %>
<%= stylesheet_link_tag "lexxy", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
エージェントによくある間違い: bin/rails action_text:install の実行を忘れることです。実行しないと、マイグレーション、JS インポート、ビューパーシャルが作成されません。has_rich_text の宣言だけでは不十分です。
ステップ 3:モデルにリッチテキストを追加する
# app/models/article.rb
class Article < ApplicationRecord
has_rich_text :content
end
主なポイント:
articlesテーブルにカラムは不要です。コンテンツはaction_text_rich_textsに保存されます。- 属性名は任意です(
:content、:body、:descriptionなど)。 - 1 つのモデルに複数のリッチテキスト属性を設定できます:
has_rich_text :bodyとhas_rich_text :summary has_rich_textごとに、個別のActionText::RichTextレコードが作成されます。
ステップ 4:フォームにエディターを追加する
<%# app/views/articles/_form.html.erb %>
<%= form_with model: @article do |form| %>
<div class="field">
<%= form.label :content %>
<%= form.rich_text_area :content %>
</div>
<% end %>
コントローラーで属性を許可します:
class ArticlesController < ApplicationController
def create
@article = Article.create!(article_params)
redirect_to @article
end
private
def article_params
params.expect(article: [:title, :content])
end
end
注: rich_text_area(または rich_textarea)は、どちらも使用できます。リッチテキストコンテンツは単一の文字列パラメーターです。特別なネストは必要ありません。
ステップ 5:リッチテキストコンテンツをレンダリングする
<%# Safe — Action Text sanitizes content automatically %>
<%= @article.content %>
以上です。ActionText::RichText#to_s は、直接埋め込んでも安全なサニタイズ済み HTML を返します。
プレーンテキストの場合(抜粋、メタディスクリプションなど):
@article.content.to_plain_text
# => "Hello world. This is bold text."
# Truncated excerpt
truncate(@article.content.to_plain_text, length: 150)
コンテンツの有無を確認する:
@article.content.blank? # true if no content
@article.content.present? # true if has content
ステップ 6:エディターとコンテンツのスタイルを設定する
Lexxy(Rails 8.1 以降)
エディターのサイズ設定:
lexxy-editor,
.lexxy-editor {
min-height: 300px;
}
レンダリングされたコンテンツのスタイル設定(.lexxy-content ラッパーを使用):
<%# app/views/layouts/action_text/contents/_content.html.erb %>
<div class="lexxy-content">
<%= yield %>
</div>
.lexxy-content {
line-height: 1.6;
overflow-wrap: break-word;
}
.lexxy-content p { margin: 0 0 1rem; }
.lexxy-content h1, .lexxy-content h2, .lexxy-content h3 {
font-weight: 600;
line-height: 1.3;
margin: 1.5rem 0 0.75rem;
}
.lexxy-content ul, .lexxy-content ol {
margin: 0 0 1rem;
padding-left: 1.5rem;
}
.lexxy-content blockquote {
border-left: 3px solid var(--color-border);
margin: 1rem 0;
padding: 0.5rem 0 0.5rem 1rem;
color: var(--color-ink-muted);
}
.lexxy-content code {
font-size: 0.875em;
background: var(--color-surface-muted);
padding: 0.125rem 0.375rem;
border-radius: 4px;
}
.lexxy-content pre {
background: var(--color-surface-muted);
border-radius: 8px;
padding: 1rem;
overflow-x: auto;
margin: 1rem 0;
}
CSS カスタムプロパティを使ったダークモード:
:root {
--lexxy-color-canvas: var(--color-surface);
--lexxy-color-text: var(--color-ink);
--lexxy-color-link: var(--color-link);
--lexxy-color-code-bg: var(--color-surface-muted);
--lexxy-focus-ring-color: var(--color-primary);
}
Lexxy の CSS 変数一覧については、references/editors.md を参照してください。
Trix(Rails 6~8.0)
コンテンツラッパーには .trix-content を使用します:
<%# app/views/layouts/action_text/contents/_content.html.erb %>
<div class="trix-content">
<%= yield %>
</div>
app/assets/stylesheets/actiontext.css でスタイルを上書きします。
エージェントによくある間違い: Action Text のコンテンツにスタイルを設定しないことです。生の出力はスタイルが適用されていない状態で表示されます。常に .trix-content または .lexxy-content の CSS を用意してください。
ステップ 7:添付ファイルを処理する
Action Text は、ファイル添付(エディターにドラッグ&ドロップされた画像やファイル)に Active Storage を使用します。
前提条件:
- Active Storage がインストールされていること(
bin/rails active_storage:install) - 画像処理用の
libvipsまたはImageMagick - Gemfile に
image_processinggem が含まれていること
添付ファイルのレンダリングをカスタマイズする:
<%# app/views/active_storage/blobs/_blob.html.erb %>
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
<% if blob.representable? %>
<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [800, 600] : [1024, 768]) %>
<% end %>
<figcaption class="attachment__caption">
<% if caption = blob.try(:caption) %>
<%= caption %>
<% else %>
<span class="attachment__name"><%= blob.filename %></span>
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
<% end %>
</figcaption>
</figure>
ステップ 8:N+1 クエリを避ける
これは非常に重要です。 リッチテキストを読み込むと、レコードごとに追加のクエリが発生します。必ず事前読み込みしてください。
# BAD — N+1 queries (one query per article for rich text)
Article.all.each { |a| a.content.to_s }
# GOOD — Preload rich text
Article.all.with_rich_text_content
# GOOD — Preload rich text AND its embedded attachments
Article.all.with_rich_text_content_and_embeds
スコープ名は動的です。 has_rich_text :name の宣言に基づいて、with_rich_text_#{name} と with_rich_text_#{name}_and_embeds になります。
# If you have: has_rich_text :body
Article.with_rich_text_body
Article.with_rich_text_body_and_embeds
# If you have: has_rich_text :description
Product.with_rich_text_description_and_embeds
エージェントによくあるミス: _and_embeds を忘れることです。これがないと、レンダリング時に添付画像の追加クエリが発生します。
ステップ 9:カスタム添付可能オブジェクト(埋め込み)
Signed GlobalID を使って、Active Record モデルをリッチテキスト内に埋め込めます。
# app/models/user.rb
class User < ApplicationRecord
include ActionText::Attachable
def to_attachable_partial_path
"users/mention"
end
end
<%# app/views/users/_mention.html.erb %>
<span class="user-mention">@<%= user.name %></span>
プログラムから挿入する場合:
user = User.find(1)
html = %(<action-text-attachment sgid="#{user.attachable_sgid}"></action-text-attachment>)
article.update!(content: "Hello #{html}")
レコードが削除されても適切に処理する:
class User < ApplicationRecord
include ActionText::Attachable
def self.to_missing_attachable_partial_path
"users/missing_mention"
end
end
<%# app/views/users/missing_mention.html.erb %>
<span class="user-mention user-mention--deleted">@deleted user</span>
ステップ 10:リッチテキストのテスト
モデルテスト:
require "test_helper"
class ArticleTest < ActiveSupport::TestCase
test "accepts rich text content" do
article = Article.new(title: "Test", content: "<h1>Hello</h1><p>World</p>")
assert article.content.present?
assert_includes article.content.to_plain_text, "Hello"
assert_includes article.content.to_plain_text, "World"
end
test "content is blank when not set" do
article = Article.new(title: "Test")
assert article.content.blank?
end
end
リクエストテスト:
require "test_helper"
class ArticlesRequestTest < ActionDispatch::IntegrationTest
test "creates article with rich text" do
assert_difference "Article.count", 1 do
post articles_path, params: {
article: { title: "Test", content: "<p>Rich text body</p>" }
}
end
assert_equal "Rich text body", Article.last.content.to_plain_text
end
end
システムテスト(エディター操作用):
require "application_system_test_case"
class ArticlesSystemTest < ApplicationSystemTestCase
test "creates article with rich text editor" do
visit new_article_path
fill_in "Title", with: "My Article"
# Fill the rich text editor
find("trix-editor").click
find("trix-editor").set("Hello from the editor")
click_on "Create Article"
assert_text "Hello from the editor"
end
end
UUID 主キー
モデルで UUID を使用している場合は、Action Text のマイグレーションを更新してください。
# In the generated migration
t.references :record, null: false, polymorphic: true, index: false, type: :uuid
コンテンツのセキュリティ
Action Text は、許可リスト方式を使ってレンダリング時に HTML をサニタイズします。許可されたタグと属性のみが通過します。手動で sanitize を呼び出す必要はありません。
カスタムサニタイズ(必要な場合):
# config/application.rb
config.action_text.sanitizer_allowed_tags = ActionText::ContentHelper::ALLOWED_TAGS + ["iframe"]
config.action_text.sanitizer_allowed_attributes = ActionText::ContentHelper::ALLOWED_ATTRIBUTES + ["src", "frameborder"]
⚠️ 許可リストを拡張する際は、十分に注意してください。 <iframe> や <script> を追加すると、XSS の脆弱性につながります。すべてのコンテンツ作成者を信頼できる場合に限り、追加してください。
クイックリファレンス
基本的なコマンド
bin/rails action_text:install # Install Action Text
bin/rails db:migrate # Run migrations
bin/rails active_storage:install # Install Active Storage (if not present)
モデルの宣言
has_rich_text :content # Single rich text field
has_rich_text :body # Name it whatever you want
has_rich_text :content # Multiple fields OK
has_rich_text :summary # on the same model
フォームヘルパー
<%= form.rich_text_area :content %>
<%= form.rich_text_area :content, placeholder: "Write something..." %>
<%= form.rich_text_area :content, data: { controller: "editor" } %>
レンダリング
<%= @article.content %> # Safe HTML
<%= @article.content.to_plain_text %> # Plain text
<%= truncate(@article.content.to_plain_text, length: 200) %> # Excerpt
事前読み込み(N+1 の防止)
Model.with_rich_text_fieldname # Preload text only
Model.with_rich_text_fieldname_and_embeds # Preload text + attachments
主要なファイル
| ファイル | 目的 |
|---|---|
app/views/layouts/action_text/contents/_content.html.erb | コンテンツのラッパー(.trix-content または .lexxy-content) |
app/views/active_storage/blobs/_blob.html.erb | 添付ファイルのレンダリングテンプレート |
app/assets/stylesheets/actiontext.css | Action Text のデフォルトスタイル |
避けるべきアンチパターン
- インストールジェネレーターを実行しない —
action_text:installを実行せずにhas_rich_textを使うと、テーブル、JS、パーシャルが不足します - コンテンツのスタイル設定をしない — Action Text の出力には、
.trix-content/.lexxy-content用の CSS が必要です - N+1 を無視する — リストビューでは必ず
with_rich_text_X_and_embedsを使ってください - モデルにカラムを追加する — リッチテキストはモデルのテーブルではなく、
action_text_rich_textsに保存されます - 手動で HTML をサニタイズする — Action Text が処理するため、二重にサニタイズするとコンテンツが壊れます
- Lexxy のスタイルシートの順序を間違える — Lexxy の CSS はアプリの CSS より前に読み込む必要があります
- Active Storage の依存関係が不足している —
libvipsがないと、画像のレンダリングが壊れます - 添付可能オブジェクトが見つからない場合の処理をしない —
to_missing_attachable_partial_pathがないと、削除されたレコードは空のボックスとして表示されます