rails-cto-erb
Skill15 starsChanged 7 months agoMITView on GitHub
How to write clean, well-formatted ERB templates for this Rails 8 project. Use when creating or modifying any ERB view, partial, layout, or component template. Also use when the user mentions "erb", "views", "partials", "templates", "html formatting", "attribute alignment", or asks about view-layer code. Proactively apply these rules whenever touching .html.erb files, even if the user doesn't explicitly ask for formatting help.
In this repository17 other files it gives its agents.
- claude/CLAUDE.mdCLAUDE.md
- rails-cto-apiSkill
- rails-cto-architectSkill
- rails-cto-commitSkill
- rails-cto-engineerSkill
- rails-cto-minitestSkill
- rails-cto-production-prSkill
- rails-cto-pull-requestSkill
- rails-cto-qaSkill
- rails-cto-restfulSkill
- rails-cto-securitySkill
- rails-ctoSkill
- rails-cto-static-analysisSkill
- rails-cto-stimulusSkill
- rails-cto-tailwindSkill
- rails-upgradeSkill
- rails-viewcomponentsSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
22 rules. About 0 are common, 13 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈13
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 20
Indent consistently at 2 spaces: No tabs, no 4-space blocks.
- Very specific: line 21
Comment sections, not lines: Use <%# ... %> to label logical groups.
- Very specific: line 27
You MUST run these steps every time this skill is invoked: After modifying any .html.erb file, run the Herb linter and formatter on the …
- Distinctive: line 35
If the command fails, inform the user:
- Distinctive: line 54
If Herb is not available, skip steps 2, 3, and 4 below and continue with the rest of the ERB skill.
- Distinctive: line 65
If either file is missing, tell the user to run:
- Very specific: line 75
Use git to find only the .erb files that were modified, not the entire codebase:
- Distinctive: line 81
If there are unstaged changes too:
- Distinctive: line 109
When an HTML element has more than one attribute, break each attribute onto its own line.
- Distinctive: line 214
When you see an inline variable in ERB, move it:
- Distinctive: line 265
Keep these at the very top of the file, right after the header comment.
- Very specific: line 271
Never use style="..." attributes in ERB templates.
- Distinctive: line 299
If the value truly cannot map to a Tailwind class (e.g., pixel-precise positioning from user data), extract it to a ViewComponent or helper …
- Distinctive: line 305
Every ERB partial follows this structure:
- Distinctive: line 345
Always 2 spaces. Nested elements increase indentation by one level:
- Very specific: line 383
Keep classes on the same line as class= unless the line exceeds ~100 characters.
- Very specific: line 410
Use <%# ... %> for comments.
- Distinctive: line 420
Use multi-line ERB comments for file headers and documentation:
- Very specific: line 431
Avoid HTML comments (<!-- -->) for documentation — they leak into the rendered output.
- Very specific: line 470
Every UI change must look correct in both light mode and dark mode.
- Distinctive: line 480
After making view changes, visually verify the UI in both modes before considering the work done.
- Very specific: line 484
Avoid duplicating markup across views.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
ERB Templates in This Project
ERB files are the most-read files in the codebase. Developers scan them constantly to understand layout, wiring, and data flow. Every formatting decision should optimize for scannability — a developer glancing at a file should immediately see the structure, the Stimulus wiring, and the data being rendered.
The Golden Rules
- No logic in templates. ERB files render — they do not compute, query, or decide.
- Align attributes vertically. Multi-attribute elements get one attribute per line.
- Indent consistently at 2 spaces. No tabs, no 4-space blocks.
- Comment sections, not lines. Use
<%# ... %>to label logical groups.
Pre-flight: Lint and Format ERB Files (MANDATORY)
You MUST run these steps every time this skill is invoked. After modifying any .html.erb file, run the Herb linter and formatter on the changed files. Herb catches syntax issues and enforces consistent formatting that manual review can miss.
1. Check if Herb is available
bundle exec herb --version 2>/dev/nullIf the command fails, inform the user:
"This project doesn't have the
herbgem installed. The recommended way to set up Herb (and the rest of the rails-cto toolchain) is to install the companionrails-ctogem:Gemfile:
group :development, :test do gem "rails-cto" endThen run:
bundle install bundle exec rails-cto initThat installs Herb along with the bundled rewriters and rules. You'll also need
@herb-tools/formatterand@herb-tools/linterinpackage.jsondevDependencies."
If Herb is not available, skip steps 2, 3, and 4 below and continue with the rest of the ERB skill. Do not block on Herb installation. But if Herb IS available, you MUST run steps 2, 3, and 4 — do not skip them.
2. Verify the bundled rewriters and rules are installed
The rails-cto gem ships the attribute alignment rewriter and the no-inline-styles rule. Check that rails-cto init has been run in the project:
ls .herb/rewriters/align-attributes.mjs 2>/dev/null
ls .herb/rules/no-inline-styles.mjs 2>/dev/nullIf either file is missing, tell the user to run:
bundle exec rails-cto initThat drops the rewriters and rules into place (skipping any files that already exist). Pass --force to overwrite.
3. Identify changed ERB files
Use git to find only the .erb files that were modified, not the entire codebase:
git diff --name-only --diff-filter=ACMR HEAD | grep '\.erb$'If there are unstaged changes too:
git diff --name-only --diff-filter=ACMR | grep '\.erb$'4. Run Herb on changed files
For each changed .erb file, run the linter with auto-fix first, then the formatter:
bundle exec herb --fix path/to/changed_file.html.erb
bundle exec herb format path/to/changed_file.html.erbFor multiple files:
bundle exec herb --fix app/views/posts/index.html.erb app/views/posts/_post.html.erb
bundle exec herb format app/views/posts/index.html.erb app/views/posts/_post.html.erbReview the output for any issues that couldn't be auto-fixed — these need manual attention. Fix ALL warnings in the file, not just ones you introduced. If Herb reports pre-existing issues unrelated to your changes, fix them anyway. Every file you touch should be left with zero Herb warnings.
Attribute Alignment
When an HTML element has more than one attribute, break each attribute onto its own line. Align attributes with the first attribute after the tag name:
<%# CORRECT — attributes aligned vertically %>
<div class="pane-field-group"
data-controller="posts--tag-sync"
data-posts--tag-sync-url-value="<%= sync_post_taggings_path(post) %>"
data-action="forms--combo-select:change->posts--tag-sync#sync"><%# WRONG — everything crammed on one line %>
<div class="pane-field-group" data-controller="posts--tag-sync" data-posts--tag-sync-url-value="<%= sync_post_taggings_path(post) %>" data-action="forms--combo-select:change->posts--tag-sync#sync">A single-attribute element can stay on one line:
<div class="flex-1 px-6 py-6 space-y-5">Render calls
For ViewComponent and partial renders with multiple arguments, align parameters with the opening parenthesis:
<%= render(Forms::EditableField.new(model: post,
field_type: :text,
attribute: :title,
url: post_url,
field_arguments: { class: "text-base font-medium" })) %>For form_with and similar helpers:
<%= form_with(model: post, method: :patch,
class: "space-y-5",
data: { controller: "utils--autosave" }) do |f| %>Buttons and interactive elements
<button type="button"
class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs"
data-action="click->combo-suggestions#add"
data-id="<%= item.id %>"
data-name="<%= item.name %>"
title="Add tag">
<%= item.name %>
</button><input> and form fields
<input type="file"
accept="image/*"
data-avatar-upload-target="input"
data-action="change->avatar-upload#selectFile"
class="hidden">No Logic in ERB
ERB files should contain zero business logic, calculations, or variable declarations beyond local_assigns.fetch defaults.
Why: Testability
Inline variables in ERB are not testable. You cannot unit-test a local variable assigned inside a template. When logic lives in a controller (instance variable), model method, or helper, it can be tested independently with Minitest. This is the primary reason inline variables are banned — if it can't be tested, it shouldn't be in ERB.
What belongs in ERB
- Rendering components and partials
- Iterating over collections passed from the controller
- Simple conditionals that control visibility (
if @items.any?) - Reading locals via
local_assigns.fetch - Output helpers (
link_to,image_tag,icon,t())
What does NOT belong in ERB
Move these to controllers, concerns, helpers, or model methods — where they can be tested.
Inline variable assignments (NEVER):
Any <% variable = ... %> line in ERB (other than local_assigns.fetch) is wrong. It doesn't matter how simple the assignment is — if it's not a partial local default, it belongs in the controller or a helper.
<%# WRONG — inline variables are not testable %>
<% tags = post.tags.where(account_id: current_account.id).order(:name) %>
<% post_count = current_account.posts.count %>
<% suggested = Tag.where(category: :interest).limit(8) %>
<% display_name = "#{user.first_name} #{user.last_name}".strip %>
<% show_banner = current_account.trial? && current_account.days_remaining < 7 %>
<%# RIGHT — instance variables from the controller, testable in controller tests %>
<%= render partial: "tag", collection: @tags %>
<%= @post_count %>
<%= current_account.display_name %>
<%= render "shared/trial_banner" if @show_trial_banner %>When you see an inline variable in ERB, move it:
- Data/queries → controller sets an instance variable (
@tags,@post_count) - Formatting/display → model method (
user.display_name) or helper (format_count(total)) - Conditional flags → controller sets a boolean instance variable (
@show_trial_banner) - Computed values → model method or concern (
account.days_remaining_display)
Math and calculations:
<%# WRONG — arithmetic in the template, inline styles %>
<span><%= (post.reading_time / 60.0).ceil %> min read</span>
<span><%= ((completed.to_f / total) * 100).round %>% complete</span>
<div style="width: <%= (tag.taggings_count.to_f / max_count * 100).round %>%">
<%# RIGHT — computed in controller or model, Tailwind classes instead of inline styles %>
<span><%= post.reading_time_display %></span>
<span><%= @completion_percentage %>% complete</span>
<div class="<%= tag.weight_class %>">Database queries:
<%# WRONG — querying in the template %>
<% recent_tags = Tag.where(account_id: current_account.id).order(updated_at: :desc).limit(5) %>
<%# RIGHT — queried in controller, passed to view %>
<% @recent_tags.each do |tag| %>String manipulation and formatting:
<%# WRONG — formatting logic in the template %>
<span><%= post.url.gsub(/^https?:\/\//, '').truncate(40) %></span>
<%# RIGHT — use a helper or model method %>
<span><%= post.display_url %></span>Acceptable local defaults
The one exception is reading partial locals with defaults at the top of a partial:
<%# This is fine — declaring expected locals with fallback values %>
<% context = local_assigns.fetch(:context, nil) %>
<% active_tab = local_assigns.fetch(:active_tab, :read) %>
<% extra_attrs ||= {} %>Keep these at the very top of the file, right after the header comment.
No Inline Styles
Never use style="..." attributes in ERB templates. Use Tailwind CSS utility classes instead. Inline styles bypass the design system, can't be purged, don't support responsive or dark mode variants, and make templates harder to scan.
The no-inline-styles.mjs Herb rule (shipped by the rails-cto gem) flags these automatically during the lint step.
<%# WRONG — inline styles %>
<div style="display: flex; gap: 8px; padding: 16px;">
<div style="width: 50%">
<span style="color: red; font-weight: bold;">Error</span>
<div style="margin-top: 1rem; border-bottom: 1px solid #e5e7eb;">
<%# RIGHT — Tailwind utility classes %>
<div class="flex gap-2 p-4">
<div class="w-1/2">
<span class="text-red-600 font-bold">Error</span>
<div class="mt-4 border-b border-gray-200 dark:border-gray-700">Dynamic widths and computed values — when the width depends on data (e.g., progress bars), use a model method that returns a Tailwind class:
<%# WRONG — inline style for dynamic width %>
<div style="width: <%= @progress %>%">
<%# RIGHT — model returns a Tailwind class like "w-1/4", "w-1/2", "w-3/4", "w-full" %>
<div class="<%= @progress_width_class %>">If the value truly cannot map to a Tailwind class (e.g., pixel-precise positioning from user data), extract it to a ViewComponent or helper that encapsulates the style — never put it inline in a template.
File Structure
Every ERB partial follows this structure:
<%# Path comment and description of what this partial does %>
<%# Additional notes about accepted locals if needed %>
<% local_defaults = local_assigns.fetch(:local_defaults, nil) %>
<%# Section Label %>
<div class="...">
<%# Subsection Label %>
<div class="...">
<%= render SomeComponent.new(...) %>
</div>
</div>- Header comment — file path and/or description of purpose
- Local defaults —
local_assigns.fetchcalls (if any) - Markup — organized into labeled sections
Documenting partials
For partials that accept locals, document them in the header:
<%# Shared split-pane layout for reading-pane controller.
Locals:
frame_id — turbo frame ID for the reader pane
extra_attrs — optional hash of extra data attributes
Block: rendered as list pane content
%>
<% extra_attrs ||= {} %>HTML Formatting
Indentation
Always 2 spaces. Nested elements increase indentation by one level:
<div class="outer">
<div class="inner">
<span class="text"><%= @value %></span>
</div>
</div>Conditionals
Indent content inside <% if %> blocks:
<% if @post.feed.present? %>
<div class="pane-field-group">
<label class="pane-field-label">Source</label>
<div class="flex items-center gap-2">
<%= inline_icon(:lucide_rss, "w-4 h-4") %>
<span class="truncate"><%= @post.feed.name %></span>
</div>
</div>
<% end %>Iterators
<div class="flex flex-wrap gap-2">
<% @tags.each do |tag| %>
<span class="tag-chip"><%= tag.name %></span>
<% end %>
</div>Long Tailwind class strings
Keep classes on the same line as class= unless the line exceeds ~100 characters. When wrapping, keep the continuation on the next line indented under the opening quote:
<%# Single line — fits comfortably %>
<div class="flex items-center gap-2 px-3 py-2 rounded-lg">
<%# Wrapped — long class list %>
<button class="inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium
bg-violet-50 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300
hover:bg-violet-100 dark:hover:bg-violet-900/50 transition-colors cursor-pointer"
data-action="click->combo-suggestions#add">Closing tags
<% end %> aligns with its opening <% if %>, <% each %>, or <%= form_with %>:
<%= form_with(model: @post) do |f| %>
<%= f.text_field :title %>
<% end %>ERB Comments
Use <%# ... %> for comments. Place them immediately before the element or section they describe:
<%# Title (editable) %>
<div class="pane-field-group">
<%# Collection %>
<%= form_with(model: post) do |f| %>Use multi-line ERB comments for file headers and documentation:
<%# Shared split-pane layout for reading-pane controller.
Locals:
frame_id — turbo frame ID
extra_attrs — optional hash of extra data attributes
Block: rendered as list pane content
%>Avoid HTML comments (<!-- -->) for documentation — they leak into the rendered output. Use them only for IE conditionals or legacy compatibility.
Stimulus Wiring in ERB
Data attributes for Stimulus should follow the same vertical alignment rules. Group them logically: controller first, then values, then targets, then actions:
<div class="pane-field-group"
data-controller="collections--combo-select-create"
data-collections--combo-select-create-create-url-value="<%= collections_path(format: :json) %>"
data-collections--combo-select-create-search-url-value="<%= search_collections_path %>"
data-action="forms--combo-select:change->utils--autosave#save">Order: class first, then data-controller, then data-*-value, then data-*-target, then data-action, then id/title/other attributes.
Turbo Frames and Streams
<%= turbo_frame_tag frame_id,
data: { reading_pane_target: "readerFrame" } do %>
<% end %><%= turbo_stream.replace dom_id(@post) do %>
<%= render partial: "posts/post",
locals: { post: @post } %>
<% end %>Light Mode and Dark Mode
Every UI change must look correct in both light mode and dark mode. When adding or modifying Tailwind classes, always include the dark: variant for colors, backgrounds, borders, and text:
<%# WRONG — only light mode %>
<div class="bg-white text-gray-900 border-gray-200">
<%# RIGHT — both modes %>
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 border-gray-200 dark:border-gray-700">After making view changes, visually verify the UI in both modes before considering the work done.
Shared Partials
Avoid duplicating markup across views. If the same UI pattern appears in more than one place, extract it into a shared partial under app/views/shared/ or a ViewComponent. Before creating a new partial, check if one already exists that does what you need.
<%# WRONG — same card markup copy-pasted in index.html.erb and show.html.erb %>
<%# RIGHT — extract to a shared partial %>
<%= render partial: "posts/post_card", locals: { post: post } %>Quick Reference: Do vs Don't
| Do | Don't |
Set @suggested_tags in the controller | Query Tag.where(...) in ERB |
Use post.reading_time_display | Write (post.reading_time / 60.0).ceil in ERB |
| Align attributes vertically | Cram 4+ attributes on one line |
Use <%# ... %> for comments | Use <!-- --> for documentation |
| Put local defaults at top of partial | Scatter variable assignments throughout |
Use local_assigns.fetch(:key, default) | Use complex ternaries for defaults |
| Pass data as locals or instance vars | Compute derived values inline |
| Use Tailwind classes for all styling | Use style="..." inline attributes |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

