This skill should be used when the user asks about "rich text", "Action Text", "Trix editor", "WYSIWYG", "has_rich_text", "content editing", "embedded attachments", "formatted text", "text editor", or needs guidance on implementing rich text editing in Rails applications.
/plugin marketplace add bastos/rails-plugin/plugin install bastos-ruby-on-rails@bastos/rails-pluginThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Comprehensive guide to rich text content with the Trix editor in Rails.
rails action_text:install
rails db:migrate
This creates:
active_storage tables (if not present)action_text_rich_texts table// app/javascript/application.js
import "trix"
import "@rails/actiontext"
// app/assets/stylesheets/application.scss
@import "trix/dist/trix";
// Or in application.css
//= require trix
//= require actiontext
class Article < ApplicationRecord
has_rich_text :content
end
class Article < ApplicationRecord
has_rich_text :content
has_rich_text :summary
has_rich_text :notes
end
class Article < ApplicationRecord
has_rich_text :content, encrypted: true
end
<%= form_with model: @article do |form| %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :content %>
<%= form.rich_text_area :content %>
</div>
<%= form.submit %>
<% end %>
<%= form.rich_text_area :content, placeholder: "Write your article here..." %>
<%= form.rich_text_area :content, class: "custom-editor", data: { controller: "editor" } %>
class ArticlesController < ApplicationController
def create
@article = Article.new(article_params)
# ...
end
private
def article_params
params.require(:article).permit(:title, :content)
end
end
<%# Renders as HTML %>
<%= @article.content %>
<%# With wrapper div %>
<div class="prose">
<%= @article.content %>
</div>
<%# Plain text version %>
<%= @article.content.to_plain_text %>
<%# Truncated plain text %>
<%= truncate(@article.content.to_plain_text, length: 200) %>
<% if @article.content.present? %>
<%= @article.content %>
<% else %>
<p class="empty">No content yet.</p>
<% end %>
<%# Or %>
<% if @article.content.blank? %>
<p>Write something!</p>
<% end %>
Action Text automatically handles image attachments through Active Storage:
# Images are automatically embedded when pasted or dragged into editor
# They're stored via Active Storage
# app/models/user.rb
class User < ApplicationRecord
include ActionText::Attachable
def to_trix_content_attachment_partial_path
"users/mention"
end
end
<%# app/views/users/_mention.html.erb %>
<span class="mention">@<%= user.name %></span>
# Attach a user mention
article.content = ActionText::Content.new("<div>Hello #{user.attachable_sgid}</div>")
# Using the helper
article.update(content: "<div>Check out #{ActionText::Attachment.from_attachable(user)}</div>")
<%# Display all attachments from rich text %>
<% @article.content.attachments.each do |attachment| %>
<% if attachment.attachable.is_a?(ActiveStorage::Blob) %>
<%= image_tag attachment.attachable.representation(resize_to_limit: [200, 200]) %>
<% end %>
<% end %>
// app/javascript/controllers/trix_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
// Remove unwanted toolbar buttons
this.element.addEventListener("trix-initialize", () => {
const toolbar = this.element.previousElementSibling
toolbar.querySelector(".trix-button-group--file-tools")?.remove()
})
}
}
<div data-controller="trix">
<%= form.rich_text_area :content %>
</div>
// Remove specific buttons
document.addEventListener("trix-initialize", (event) => {
const toolbar = event.target.toolbarElement
// Remove file attachment button
toolbar.querySelector('[data-trix-action="attachFiles"]')?.remove()
// Remove heading button
toolbar.querySelector('[data-trix-attribute="heading1"]')?.remove()
})
// Add custom button to toolbar
document.addEventListener("trix-initialize", (event) => {
const toolbar = event.target.toolbarElement
const buttonGroup = toolbar.querySelector(".trix-button-group--block-tools")
const button = document.createElement("button")
button.setAttribute("type", "button")
button.setAttribute("class", "trix-button")
button.setAttribute("data-trix-attribute", "highlight")
button.textContent = "Highlight"
buttonGroup.appendChild(button)
})
// Define the attribute
Trix.config.textAttributes.highlight = {
tagName: "mark",
inheritable: true
}
// Customize Trix appearance
trix-editor {
min-height: 300px;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 4px;
&:focus {
border-color: #3b82f6;
outline: none;
}
}
trix-toolbar {
background: #f9fafb;
border-bottom: 1px solid #ddd;
padding: 0.5rem;
}
// Style the rendered content
.trix-content {
h1 { font-size: 1.5rem; margin-top: 1rem; }
blockquote {
border-left: 3px solid #ddd;
padding-left: 1rem;
color: #666;
}
pre {
background: #f4f4f5;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
}
// Listen for content changes
document.addEventListener("trix-change", (event) => {
const editor = event.target
console.log("Content changed:", editor.value)
})
// Before paste
document.addEventListener("trix-before-paste", (event) => {
// Modify paste behavior
})
// Before file accept
document.addEventListener("trix-file-accept", (event) => {
// Validate file
const acceptedTypes = ["image/jpeg", "image/png", "image/gif"]
if (!acceptedTypes.includes(event.file.type)) {
event.preventDefault()
alert("Only images are allowed!")
}
// Limit file size (5MB)
if (event.file.size > 5 * 1024 * 1024) {
event.preventDefault()
alert("File too large!")
}
})
// After file attached
document.addEventListener("trix-attachment-add", (event) => {
const attachment = event.attachment
if (attachment.file) {
uploadFile(attachment)
}
})
// app/javascript/controllers/rich_text_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["editor", "counter"]
connect() {
this.updateCounter()
}
change(event) {
this.updateCounter()
this.autoSave()
}
updateCounter() {
const text = this.editorTarget.editor.getDocument().toString()
this.counterTarget.textContent = `${text.length} characters`
}
autoSave() {
clearTimeout(this.saveTimer)
this.saveTimer = setTimeout(() => {
this.save()
}, 2000)
}
save() {
// Auto-save logic
}
}
# Find articles containing text
Article.joins(:rich_text_content)
.where("action_text_rich_texts.body LIKE ?", "%search term%")
# Scope for searching
class Article < ApplicationRecord
has_rich_text :content
scope :search_content, ->(term) {
joins(:rich_text_content)
.where("action_text_rich_texts.body LIKE ?", "%#{term}%")
}
end
# Avoid N+1 queries
@articles = Article.all.with_rich_text_content
# Multiple rich text fields
@articles = Article.all.with_rich_text_content.with_rich_text_summary
# With embedded images
@articles = Article.all.with_rich_text_content_and_embeds
# test/models/article_test.rb
require "test_helper"
class ArticleTest < ActiveSupport::TestCase
test "has rich text content" do
article = Article.new(title: "Test", content: "<p>Hello World</p>")
assert article.content.present?
assert_includes article.content.to_plain_text, "Hello World"
end
test "content with attachment" do
article = articles(:one)
blob = active_storage_blobs(:image)
article.content.body.attachables << blob
article.save!
assert_equal 1, article.content.body.attachments.count
end
end
# spec/models/article_spec.rb
require "rails_helper"
RSpec.describe Article, type: :model do
describe "rich text" do
it "stores content" do
article = create(:article, content: "<p>Test content</p>")
expect(article.content.to_plain_text).to include("Test content")
expect(article.content.body.to_html).to include("<p>")
end
it "handles attachments" do
article = create(:article)
article.content.body.attachables << create(:blob)
expect(article.content.body.attachments).not_to be_empty
end
end
end
# test/system/articles_test.rb
require "application_system_test_case"
class ArticlesTest < ApplicationSystemTestCase
test "creating article with rich text" do
visit new_article_path
fill_in "Title", with: "My Article"
# Fill in Trix editor
find("trix-editor").click.set("This is rich text content")
click_button "Create Article"
assert_text "Article was successfully created"
assert_text "This is rich text content"
end
end
Action Text automatically sanitizes content. Customize allowed tags:
# config/initializers/action_text.rb
Rails.application.config.after_initialize do
ActionText::ContentHelper.allowed_tags = %w[
strong em del a h1 h2 h3 h4 blockquote pre code ul ol li
]
ActionText::ContentHelper.allowed_attributes = %w[
href class data-*
]
end
# Sanitize on output (additional layer)
<%= sanitize @article.content.to_s, tags: %w[p strong em a], attributes: %w[href] %>
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.