Entity System

Manage any ActiveRecord model through the RailsPress admin interface without writing custom controllers or views.

Quick Start

1. Include the Entity concern in your model

app/models/project.rb
class Project < ApplicationRecord
  include Railspress::Entity

  has_rich_text :body
  has_many_attached :gallery

  # Declare which fields appear in the CMS
  railspress_fields :title, :client, :featured
  railspress_fields :description
  railspress_fields :body
  railspress_fields :gallery, as: :attachments

  # Optional: Custom sidebar label
  railspress_label "Client Projects"

  validates :title, presence: true
end

2. Register the entity in an initializer

config/initializers/railspress.rb
Railspress.configure do |config|
  config.register_entity :project
end

That's it. Your model now has full CRUD at /railspress/admin/entities/projects.

The railspress_fields DSL

Declare which model attributes should appear in the admin forms and index table.

Basic usage (auto-detected types)

Ruby
railspress_fields :title, :description, :published

Types are automatically detected from:

  1. ActionText associations (has_rich_text)
  2. ActiveStorage attachments (has_one_attached, has_many_attached)
  3. Database column types

Explicit type override

Ruby
railspress_fields :body, as: :rich_text
railspress_fields :gallery, as: :attachments

Supported Field Types

Type Detection Form Input Index Display
:string String columns Text field Truncated text
:text Text columns Textarea Truncated text
:rich_text has_rich_text Trix editor Stripped/truncated
:boolean Boolean columns Checkbox Yes/No badge
:integer Integer columns Number field Raw value
:decimal Decimal/float columns Number field (step: any) Raw value
:datetime Datetime columns Datetime-local picker Formatted date
:date Date columns Date picker Formatted date
:attachment has_one_attached File input Attached/None badge
:attachments has_many_attached Multiple file input "N images" badge
:focal_point_image focal_point_image macro Image upload + focal picker Attached/None badge
:list Explicit only Text field (comma-separated) "N items" badge
:lines Explicit only Textarea (line-separated) "N items" badge

Form layout

Fields are automatically organized into a two-column layout:

  • Main column: String, text, rich text, and :lines fields
  • Sidebar: Boolean, numeric, date, and :list fields in an "Options" section
  • Sidebar: Each attachment field gets its own section with preview and removal

Array Fields

Store arrays of strings in JSON columns using :list and :lines field types. These are useful for things like tech stacks, feature lists, or highlights.

Quick Start

app/models/project.rb
class Project < ApplicationRecord
  include Railspress::Entity

  railspress_fields :title, :client
  railspress_fields :tech_stack, as: :list      # Comma-separated input
  railspress_fields :highlights, as: :lines     # Line-separated input
end

Virtual attributes are auto-generated. No extra concerns or boilerplate needed.

Migration

Use JSON or JSONB columns with array defaults:

db/migrate/add_array_fields_to_projects.rb
class AddArrayFieldsToProjects < ActiveRecord::Migration[8.0]
  def change
    add_column :projects, :tech_stack, :jsonb, default: [], null: false
    add_column :projects, :highlights, :jsonb, default: [], null: false
  end
end

Two Field Types

Type Input Format Best For Deduplicates?
:list Comma-separated Short items (tags, tech names) Yes
:lines One per line Sentences, paragraphs No

How It Works

When you declare railspress_fields :tech_stack, as: :list, RailsPress auto-generates:

Auto-generated methods
# Nil guard - always returns array, never nil
def tech_stack
  super || []
end

# Virtual getter: array → comma string (for form population)
def tech_stack_list
  tech_stack.join(", ")
end

# Virtual setter: comma string → array (for form submission)
def tech_stack_list=(value)
  self.tech_stack = value.split(",").map(&:strip).reject(&:blank?).uniq
end

For :lines fields, the separator is newline instead of comma, and duplicates are preserved.

Form Behavior

:list fields render as a single-line text input with a hint to separate items with commas.

:lines fields render as a textarea with a hint to enter one item per line.

Display Behavior

  • Index view: Shows item count badge ("3 items")
  • Show view :list: Displays inline as "Ruby, Rails, PostgreSQL"
  • Show view :lines: Displays as bullet list

Input Parsing

:list fields:

  • Split on comma
  • Strip whitespace from each item
  • Remove empty items
  • Deduplicate (preserves first occurrence)
irb
project.tech_stack_list = "  Ruby ,, Rails , Ruby  "
project.tech_stack  # => ["Ruby", "Rails"]

:lines fields:

  • Split on newline (handles both \n and \r\n)
  • Strip whitespace from each item
  • Remove empty lines
  • Preserves duplicates (order matters for content like steps)

Entity Registration

config/initializers/railspress.rb
Railspress.configure do |config|
  # Symbol registration (preferred)
  config.register_entity :project
  config.register_entity :testimonial
  config.register_entity :team_member

  # Custom sidebar label
  config.register_entity :case_study, label: "Client Work"
end

Routes

Entity routes are automatically generated under /railspress/admin/entities/:entity_type:

Method Path Action
GET /entities/projects index
GET /entities/projects/new new
POST /entities/projects create
GET /entities/projects/:id show
GET /entities/projects/:id/edit edit
PATCH/PUT /entities/projects/:id update
DELETE /entities/projects/:id destroy

Attachments

Single attachment

app/models/testimonial.rb
class Testimonial < ApplicationRecord
  include Railspress::Entity

  has_one_attached :avatar

  railspress_fields :name, :quote
  railspress_fields :avatar, as: :attachment
end

Multiple attachments

app/models/project.rb
class Project < ApplicationRecord
  include Railspress::Entity

  has_many_attached :gallery

  railspress_fields :title
  railspress_fields :gallery, as: :attachments
end

Attachment fields support image preview thumbnails, individual removal checkboxes, direct upload (if configured), and adding new files while keeping existing ones.

Focal Point Images

For images that need focal point editing (hero banners, cards, OG images), use the focal_point_image macro. It combines ActiveStorage attachment, focal point support, and entity field registration in one call.

Basic Usage

app/models/project.rb
class Project < ApplicationRecord
  include Railspress::Entity
  include Railspress::HasFocalPoint

  focal_point_image :cover_image
end

With Variants

app/models/project.rb
class Project < ApplicationRecord
  include Railspress::Entity
  include Railspress::HasFocalPoint

  focal_point_image :main_image do |attachable|
    attachable.variant :hero, resize_to_fill: [2100, 900, { crop: :centre }]
    attachable.variant :card, resize_to_fill: [800, 500, { crop: :centre }]
    attachable.variant :thumb, resize_to_fill: [400, 250, { crop: :centre }]
    attachable.variant :og, resize_to_fill: [1200, 630, { crop: :centre }]
  end

  # No need to declare main_image in railspress_fields - auto-registered
  railspress_fields :title, :client, :featured
end

One call handles three things: has_one_attached (declares the ActiveStorage attachment with optional variants), has_focal_point (adds focal point editing support), and railspress_fields registration (automatic).

Using in Views

ERB
<%= image_tag url_for(@project.main_image.variant(:hero)),
      style: @project.focal_point_css(:main_image),
      class: "object-cover w-full h-full" %>

See Image Focal Points for full documentation on focal points, context overrides, and the admin UI.

Pagination & Scopes

Built-in Scopes

Every Entity includes these scopes:

Scope Description
ordered By created_at descending
recent First 10 records, ordered
page(n) Pagination helper (uses PER_PAGE)
Ruby
Project.ordered                    # All projects, newest first
Project.recent                     # Last 10 projects
Project.ordered.page(2)            # Second page of projects
Project.where(featured: true).recent  # Last 10 featured projects

Custom Page Size

Override the default page size (20) in your model:

Ruby
class Project < ApplicationRecord
  include Railspress::Entity

  PER_PAGE = 50  # Override default of 20

  railspress_fields :title, :client
end

Tagging

Make any entity taggable by including the Railspress::Taggable concern:

app/models/project.rb
class Project < ApplicationRecord
  include Railspress::Entity
  include Railspress::Taggable

  railspress_fields :title, :description
end

What Taggable Provides

Method Description
tag_list Returns tags as comma-separated string
tag_list= Sets tags from comma-separated string
tags Returns associated Railspress::Tag records
taggings Returns the join records

Form Integration

ERB
<%= rp_string_field f, :tag_list, label: "Tags", hint: "Comma-separated" %>

Tags are shared across all taggable models. A tag "ruby" used on a Post and a Project points to the same Railspress::Tag record.

Custom Index Columns

By default, entity index pages display columns based on Railspress.default_index_columns (defaults to :id, :title, :name, :created_at). Only columns that the model responds to are shown.

Overriding with a Constant

Ruby
class Project < ApplicationRecord
  include Railspress::Entity

  RAILSPRESS_INDEX_COLUMNS = [:title, :client, :status, :created_at]

  railspress_fields :title, :client, :description, :status, :featured
end

Overriding with a Method

For dynamic logic, override the railspress_index_columns class method:

Ruby
class Project < ApplicationRecord
  include Railspress::Entity

  def self.railspress_index_columns
    columns = [:title, :client, :created_at]
    columns << :budget if Current.user&.admin?
    columns
  end
end

Global Default

config/initializers/railspress.rb
Railspress.configure do |config|
  config.default_index_columns = [:title, :name, :updated_at]
end

Full Example

app/models/team_member.rb
class TeamMember < ApplicationRecord
  include Railspress::Entity

  has_one_attached :headshot
  has_rich_text :bio

  railspress_fields :name, :role, :email
  railspress_fields :bio
  railspress_fields :headshot, as: :attachment
  railspress_fields :featured, :display_order

  railspress_label "Team"

  validates :name, presence: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true

  scope :featured, -> { where(featured: true) }
  scope :ordered, -> { order(display_order: :asc, name: :asc) }
end

Admin available at: /railspress/admin/entities/team_members