From obie-skills
Adds Tiptap rich text editing with debounced autosave to Rails models using Stimulus. Stores markdown in text columns (not ActionText), handles Turbo cache, includes reusable partials and optional change tracking.
npx claudepluginhub joshuarweaver/cascade-code-general-misc-2 --plugin obie-skillsThis skill uses the workspace's default tool permissions.
Add rich text editing with automatic background saving to any Rails model using Tiptap, Stimulus, and markdown stored in plain text columns.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Add rich text editing with automatic background saving to any Rails model using Tiptap, Stimulus, and markdown stored in plain text columns.
Invoke this skill when:
Key decision: Markdown in text columns, NOT ActionText.
Why this approach:
How it works:
text column via update_column| File | Purpose |
|---|---|
app/javascript/controllers/rich_text_editor_controller.js | Tiptap Stimulus controller |
app/views/shared/_rich_text_field.html.erb | Reusable editor partial |
app/views/shared/_bubble_menu.html.erb | Formatting toolbar |
npm packages, @rails/request.js for CSRF, JS bundler setup (Tiptap does NOT work with importmap), and editor CSS.
See: references/installation.md
The full rich_text_editor_controller.js -- handles Tiptap initialization, debounced autosave, BubbleMenu target relocation, Turbo cache cleanup, and bubble menu formatting commands.
See: references/stimulus-controller.md
Reusable _rich_text_field.html.erb and _bubble_menu.html.erb partials with Stimulus data attributes.
See: references/partials.md
Debounced change tracking that groups rapid edits into single audit events.
See: references/audit-trail.md
Quick step-by-step for any model (Article, Post, Page, etc.):
class AddBodyToArticles < ActiveRecord::Migration[7.1]
def change
add_column :articles, :body, :text
end
end
Use text, NOT string. The string type has a 255-character limit -- far too small for rich text content.
# config/routes.rb
resources :articles do
member do
patch :autosave
end
end
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :autosave]
AUTOSAVE_FIELDS = %w[body summary notes].freeze
def autosave
field = params[:field].to_s
unless AUTOSAVE_FIELDS.include?(field)
return render json: { error: "field not allowed" }, status: :bad_request
end
@article.update_column(field.to_sym, params[:value])
render json: { status: "saved" }
end
end
Key details:
update_column bypasses validations and callbacks -- important for autosave performanceset_article must include :autosave in the only: list<%= render "shared/rich_text_field",
url: autosave_article_path(@article),
field: "body",
content: @article.body,
placeholder: "Write your article...",
label: "Body",
subtitle: "-- supports markdown formatting" %>
text, not string (255 char limit will silently truncate content)patch :autosave member route must exist or you get 404s on saveset_article (or equivalent) must include :autosave in its only: listbroadcasts_refreshes on models with Tiptap -- Turbo morphing destroys editor state mid-edit. If you need real-time updates, scope broadcasts to exclude the editing user.has_rich_text declarations.references/installation.md for setup.this.bubbleMenuTarget unreachable after initialization. The controller must save a reference before calling new Editor(). This is the #1 source of "target not found" errors. See references/stimulus-controller.md.turbo:before-cache handling, back-button navigation shows a broken editor that won't reinitialize. The controller must destroy the editor and restore the DOM before Turbo caches the page. See references/stimulus-controller.md.Each field gets its own controller instance via its own render call. The field value tells the autosave endpoint which column to update:
<%= render "shared/rich_text_field",
url: autosave_article_path(@article),
field: "body",
content: @article.body,
placeholder: "Article body...",
label: "Body" %>
<%= render "shared/rich_text_field",
url: autosave_article_path(@article),
field: "summary",
content: @article.summary,
placeholder: "Brief summary...",
label: "Summary" %>
Each instance is fully independent -- separate editor, separate autosave, separate status indicator.
Since content is stored as markdown, you need to render it to HTML for display. Common approaches:
# Gemfile
gem "redcarpet"
# or
gem "commonmarker"
# app/helpers/markdown_helper.rb
module MarkdownHelper
def render_markdown(text)
return "" if text.blank?
renderer = Redcarpet::Render::HTML.new(hard_wrap: true, filter_html: true)
markdown = Redcarpet::Markdown.new(renderer,
autolink: true,
tables: true,
fenced_code_blocks: true,
strikethrough: true
)
markdown.render(text).html_safe
end
end
<%# In your show view %>
<div class="prose prose-sm max-w-none">
<%= render_markdown(@article.body) %>
</div>