Rich text content powers blogs, documentation systems, and collaborative tools. Rails 8 ships with Action Text, providing a complete solution for handling rich content with the Trix editor, Active Storage attachments, and seamless database persistence.
Setting Up Action Text
Action Text requires Active Storage for handling embedded attachments. Install both with a single command:
# Terminal
bin/rails action_text:install
bin/rails db:migrateThis generates migrations for the action_text_rich_texts table and installs the Trix editor assets. With Rails 8 using import maps by default, the JavaScript dependencies load automatically.
Add the rich text field to a model:
# app/models/article.rb
class Article < ApplicationRecord
has_rich_text :body
validates :title, presence: true
validates :body, presence: true
endThe has_rich_text macro creates a polymorphic association to ActionText::RichText. No additional columns are needed on the articles tableβthe content lives in action_text_rich_texts.
Building the Form with Trix
Rails provides the rich_text_area helper that renders the Trix editor:
# app/views/articles/_form.html.erb
<%= form_with model: @article do |form| %>
<%= form.label :title %>
<%= form.text_field :title, class: "input" %>
<%= form.label :body %>
<%= form.rich_text_area :body, class: "trix-content" %>
<%= form.submit class: "btn btn-primary" %>
<% end %>The controller requires minimal setup. Permit the body attribute as a scalar value:
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article, notice: "Article created."
else
render :new, status: :unprocessable_entity
end
end
private
def article_params
params.require(:article).permit(:title, :body)
end
endRendering Rich Text Content
Display rich text content with proper sanitization using the built-in helper:
# app/views/articles/show.html.erb
<%= @article.title %>
<%= @article.body %>
Action Text automatically sanitizes HTML output, preventing XSS attacks while preserving allowed formatting. The rendered output includes a trix-content class for styling hooks.
Style the content container to match the application design:
/* app/assets/stylesheets/action_text.css */
.trix-content {
line-height: 1.6;
}
.trix-content h1 {
font-size: 1.5rem;
margin-top: 1.5rem;
}
.trix-content blockquote {
border-left: 3px solid #ccc;
padding-left: 1rem;
color: #666;
}
.trix-content pre {
background: #f4f4f4;
padding: 1rem;
overflow-x: auto;
}
.trix-content a {
color: #0066cc;
text-decoration: underline;
}Handling Embedded Attachments
Action Text integrates with Active Storage for image and file embeds. Configure Active Storage for MySQL:
# config/storage.yml
local:
service: Disk
root: <%= Rails.root.join("storage") %>
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: us-east-1
bucket: <%= Rails.application.credentials.dig(:aws, :bucket) %>When users drag images into the Trix editor, Active Storage handles the upload automatically. Control which file types and sizes are acceptable:
# app/models/article.rb
class Article < ApplicationRecord
has_rich_text :body
validates :title, presence: true
validates :body, presence: true
validate :acceptable_attachments
private
def acceptable_attachments
return unless body.body.present?
body.body.attachments.each do |attachment|
blob = attachment.attachable
next unless blob.is_a?(ActiveStorage::Blob)
unless blob.content_type.in?(%w[image/png image/jpeg image/gif image/webp])
errors.add(:body, "contains unsupported file type: #{blob.content_type}")
end
if blob.byte_size > 5.megabytes
errors.add(:body, "contains files larger than 5MB")
end
end
end
endEager Loading Rich Text
Action Text associations can cause N+1 queries when rendering lists. Use eager loading to fetch rich text content efficiently:
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article
.with_rich_text_body
.order(created_at: :desc)
.limit(20)
end
def show
@article = Article
.with_rich_text_body_and_embeds
.find(params[:id])
end
endThe with_rich_text_body scope loads the rich text content without attachments. Use with_rich_text_body_and_embeds when rendering full content with embedded images.
Plain Text Extraction
Extract plain text for search indexing, previews, or excerpts:
# app/models/article.rb
class Article < ApplicationRecord
has_rich_text :body
def excerpt(length: 200)
body.to_plain_text.truncate(length)
end
def searchable_content
[title, body.to_plain_text].join(" ")
end
endThis plain text conversion strips all HTML tags and attachment references, leaving clean content for indexing with MySQL full-text search or external search engines.
Turbo Integration for Live Preview
Combine Action Text with Turbo Streams for live preview functionality:
# app/views/articles/new.html.erb
<%= render "form", article: @article %>
Preview
<%= @article.body %>
# app/javascript/controllers/preview_controller.js
import { Controller } from "@hotwired/stimulus"
import { debounce } from "lodash-es"
export default class extends Controller {
static targets = ["editor", "preview"]
connect() {
this.updatePreview = debounce(this.updatePreview.bind(this), 300)
this.editorTarget.addEventListener("trix-change", this.updatePreview)
}
disconnect() {
this.editorTarget.removeEventListener("trix-change", this.updatePreview)
}
updatePreview() {
const content = this.editorTarget.editor.getDocument().toString()
fetch("/articles/preview", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": document.querySelector("[name='csrf-token']").content
},
body: JSON.stringify({ body: this.editorTarget.value })
})
.then(response => response.text())
.then(html => {
Turbo.renderStreamMessage(html)
})
}
}Common Mistakes to Avoid
Several patterns cause problems with Action Text implementations:
- Forgetting eager loading leads to N+1 queries on index pages. Always use with_rich_text_* scopes when displaying multiple records.
- Storing large content in the action_text_rich_texts table can slow queries. Consider archiving old content or using a separate storage strategy for high-volume applications.
- Not validating attachments allows users to upload arbitrary files. Always validate content types and file sizes.
- Styling conflicts occur when application CSS overrides Trix editor styles. Scope Trix styles carefully and test editor functionality after CSS changes.
Summary
Action Text provides a complete rich text editing solution for Rails 8 applications. The integration with Active Storage handles embedded attachments, while Trix delivers a consistent editing experience across browsers. Combine these tools with proper eager loading, attachment validation, and Turbo integration to build robust content management features without external dependencies.