Reusable Rails engine for a CMS-style admin: content blocks, visual editor, versioning, pluggable dashboard, permissions, analytics, and visitor error tracking.
Your app owns product features (pages, models, business logic); RubyCMS manages content workflows and admin screens.
- Content blocks -- rich text, plain text, images, links, and lists with locale support
- Content block versioning -- automatic version history with rollback and side-by-side diffs
- Visual editor -- inline editing on live page previews
- Pluggable dashboard -- registry-based blocks with permission gating and host-app extensibility
- Permissions -- key-based access control with templates and per-user assignment
- Admin page generator -- one command to scaffold a new admin page with nav, permission, and route
- Named icon registry -- 20+ Heroicons by name, no SVG copy-pasting
- Settings -- DB-backed admin settings with categories and UI
- Analytics -- Ahoy-powered visit/event tracking with dashboard drill-downs
- Visitor error tracking -- automatic 404/500 capture with admin overview
# Add to Gemfile
gem "ruby_cms"
# Install
rails g ruby_cms:install
rails db:migrate
rails ruby_cms:seed_permissions
rails ruby_cms:setup_adminThe install generator sets up the initializer, mounts the engine at /admin, runs migrations, configures Tailwind, Stimulus, Action Text, and Ahoy.
Visit /admin and sign in with the admin user you configured.
Use the content_block helper in any view:
<%= content_block("hero_title", default: "Welcome") %>
<%= content_block("footer", cache: true) %>| Type | Description |
|---|---|
text |
Plain text string |
rich_text |
Action Text rich content (HTML) |
image |
Attached image via Active Storage |
link |
URL string |
list |
JSON array or newline-separated items |
content_block wraps output for the visual editor. For HTML attributes, use wrap: false:
<%= text_field_tag :name, nil,
placeholder: content_block("contact.placeholder", wrap: false, fallback: "Your name") %>Or use content_block_text which never wraps:
<meta name="description" content="<%= content_block_text("meta_desc", fallback: "Default") %>"><% content_block_list_items("badges", fallback: ["Ruby", "Rails"]).each do |badge| %>
<%= tag.span badge, class: "badge" %>
<% end %>Content blocks have a locale field. The CMS groups blocks by key prefix across locales for easy management.
Versions are created automatically on every meaningful change:
block = ContentBlock.create!(key: "hero", title: "Welcome", content: "Hello",
content_type: "text", locale: "en")
block.versions.count # => 1 (event: "create")
block.update!(title: "Welcome!")
block.versions.count # => 2 (event: "update")
block.update!(published: false)
block.versions.last.event # => "unpublish"| Event | When |
|---|---|
create |
Block is first created |
update |
Title, content, or content_type changes |
publish |
published changes to true |
unpublish |
published changes to false |
rollback |
rollback_to_version! is called |
visual_editor |
Edit via the visual editor |
old_version = block.versions.first
block.rollback_to_version!(old_version, user: current_user)
# Creates a new version with event: "rollback"
# Restores title, content, content_type, published, and rich_content- Version history link on each content block show page
- Timeline view with colored event badges
- Side-by-side diff (old vs new, red/green)
- Rollback button with confirmation
GET /admin/content_blocks/:id/versions # index (HTML + JSON)
GET /admin/content_blocks/:id/versions/:vid # show with diff
POST /admin/content_blocks/:id/versions/:vid/rollback
Configure preview templates in config/initializers/ruby_cms.rb:
RubyCms.configure do |c|
c.preview_templates = { "home" => "pages/home", "about" => "pages/about" }
c.preview_data = ->(page_key, view) { { products: Product.limit(5) } }
endOpen Admin > Visual editor, pick a page, and click content blocks to edit them inline.
The dashboard uses a registry-based block system. Each block is a partial with optional permission gating and data injection.
| Block | Section | Permission |
|---|---|---|
| Content blocks stats | stats | manage_content_blocks |
| Users stats | stats | manage_permissions |
| Permissions stats | stats | manage_permissions |
| Visitor errors stats | stats | manage_visitor_errors |
| Quick actions | main | -- |
| Recent errors | main | manage_visitor_errors |
| Analytics overview | main | manage_analytics |
# config/initializers/ruby_cms.rb (or ruby_cms_pages.rb)
Rails.application.config.to_prepare do
RubyCms.dashboard_register(
key: :orders_stats,
label: "Orders",
section: :stats, # :stats (top row) or :main (bottom grid)
order: 5,
partial: "admin/dashboard/orders_stats",
permission: :manage_orders,
span: :single, # :single or :double (grid width)
data: ->(controller) {
{ count: Order.count, today: Order.where("created_at > ?", Date.today).count }
}
)
endThen create the partial app/views/admin/dashboard/_orders_stats.html.erb. The block local contains the registration data and any computed data.
Re-register with the same key to override:
RubyCms.dashboard_register(
key: :quick_actions,
label: "Quick actions",
section: :main,
partial: "admin/dashboard/my_quick_actions" # your own partial
)One call to register a nav item and its permission:
RubyCms.register_page(
key: :backups,
label: "Backups",
path: :admin_backups_path, # Symbol: auto-wrapped via main_app
icon: :archive_box, # Named icon from RubyCms::Icons
section: :main, # :main (sidebar top) or :settings (sidebar bottom)
permission: :manage_backups, # Auto-registered as permission key
order: 10
)For more control (custom visibility gates, non-standard paths):
RubyCms.nav_register(
key: :reports,
label: "Reports",
path: ->(view) { view.main_app.reports_path },
icon: :chart_bar,
section: "main",
permission: :manage_reports,
if: ->(view) { view.current_user_cms&.admin? }
)| Format | Example | Behavior |
|---|---|---|
| Symbol | :admin_backups_path |
Auto-wrapped: view.main_app.send(:admin_backups_path) |
| Lambda | -> (v) { v.some_path } |
Called with view context |
| String | "/admin/backups" |
Used as-is |
Use symbol keys from RubyCms::Icons:
RubyCms::Icons.available
# => [:home, :pencil_square, :document_duplicate, :chart_bar, :shield_check,
# :exclamation_triangle, :user_group, :cog_6_tooth, :archive_box, :folder,
# :bell, :clock, :tag, :cube, :envelope, :wrench, :globe, :photograph,
# :list_bullet, :plus_circle, :trash, :eye, :lock_closed, :currency_dollar]Raw SVG strings are also accepted for custom icons.
Default keys: manage_admin, manage_permissions, manage_content_blocks, manage_visitor_errors, manage_analytics.
Register additional keys:
RubyCms.register_permission_keys(:manage_orders, :manage_reports)Group permission keys into reusable templates:
RubyCms.register_permission_template(:editor,
label: "Editor",
keys: %w[manage_admin manage_content_blocks],
description: "Can manage content but not users"
)
# Apply to a user:
RubyCms::Permission.apply_template!(user, :editor)In controllers that inherit from RubyCms::Admin::BaseController, use cms_page to link to a registered page. The permission is looked up from the nav registry automatically:
class Admin::BackupsController < RubyCms::Admin::BaseController
cms_page :backups # permission from register_page(:backups, permission: :manage_backups)
def index
@backups = Backup.recent
end
endScaffold a complete admin page with one command:
rails g ruby_cms:admin_page backupsThis generates:
| File | Description |
|---|---|
app/controllers/admin/backups_controller.rb |
Controller with cms_page :backups |
app/views/admin/backups/index.html.erb |
View template |
config/routes.rb |
Route injection (namespace :admin) |
config/initializers/ruby_cms_pages.rb |
register_page call |
rails g ruby_cms:admin_page backups \
--permission=manage_backups \
--icon=archive_box \
--section=settings \
--order=15| Option | Default | Description |
|---|---|---|
--permission |
manage_<name> |
Permission key |
--icon |
folder |
Icon from RubyCms::Icons |
--section |
main |
main or settings |
--order |
10 |
Sort order in nav |
After generating, run rails ruby_cms:seed_permissions to create the permission row in the database.
RubyCMS uses DB-backed settings with a registry for defaults and types:
# Read
RubyCms::Settings.get(:analytics_default_period, default: "week")
# Write (admin UI or programmatic)
RubyCms::Settings.set(:analytics_default_period, "month")RubyCms::SettingsRegistry.register(
key: :my_custom_setting,
type: :string,
default: "hello",
category: :general,
description: "A custom setting"
)Settings are managed in Admin > Settings with tabs per category.
Public exceptions are automatically captured when RubyCms::VisitorErrorCapture is included in your ApplicationController (added by the install generator).
View captured errors in Admin > Visitor errors with status codes, paths, timestamps, and resolution tracking.
RubyCMS integrates with Ahoy for server-side page view and event tracking.
Include RubyCms::PageTracking in public controllers:
class PagesController < ApplicationController
include RubyCms::PageTracking
endView analytics in Admin > Analytics with:
- Visit and event counts
- Popular pages
- Top visitors
- Configurable date ranges and periods
RubyCms.configure do |c|
c.analytics_visit_scope = ->(scope) { scope.where.not(ip: ["127.0.0.1"]) }
c.analytics_event_scope = ->(scope) { scope }
c.analytics_extra_cards = lambda { |start_date:, end_date:, period:, visits_scope:, events_scope:|
[{ title: "Custom KPI", value: visits_scope.count }]
}
endSet the translation namespace:
RubyCms.configure do |c|
c.content_blocks_translation_namespace = "content_blocks"
endCreate locale files:
# config/locales/en.yml
en:
content_blocks:
hero_title: "Welcome to my site"
footer_text: "Copyright 2026"Import:
rails ruby_cms:content_blocks:importExport DB content blocks back to YAML:
rails ruby_cms:content_blocks:exportAll configuration happens in config/initializers/ruby_cms.rb:
RubyCms.configure do |c|
# Base controller (must provide current_user + authentication)
c.admin_base_controller = "ApplicationController"
# Admin layout
c.admin_layout = "admin/admin"
# User model class name
c.user_class_name = "User"
# Allow user.admin? bypass when no Permission records exist
c.bootstrap_admin_with_role = true
# Redirect path for unauthenticated/unauthorized users
c.unauthorized_redirect_path = "/"
# Visual editor
c.preview_templates = { "home" => "pages/home" }
c.preview_data = ->(page_key, view) { {} }
# Content blocks
c.content_blocks_translation_namespace = "content_blocks"
c.image_content_types = %w[image/png image/jpeg image/gif image/webp]
c.image_max_size = 5 * 1024 * 1024
end| Task | Description |
|---|---|
ruby_cms:seed_permissions |
Create default permission rows + settings |
ruby_cms:setup_admin |
Interactive first admin user setup |
ruby_cms:grant_manage_admin |
Grant all permissions to a user by email |
ruby_cms:content_blocks:export |
Export DB content blocks to YAML |
ruby_cms:content_blocks:import |
Import content blocks from YAML |
ruby_cms:content_blocks:sync |
Export + optional import |
ruby_cms:import_initializer_settings |
Import initializer values into DB settings |
ruby_cms:css:compile |
Compile admin CSS to host app |
ruby_cms:css:compile_gem |
Compile admin CSS within the gem |
git clone https://github.com/jobhammer00/ruby_cms.git
cd ruby_cms
bundle installbundle exec rspecTests use an in-memory SQLite database via spec/support/dummy_app.rb. No separate dummy Rails app directory is needed.
rails ruby_cms:css:compile_gemlib/ruby_cms.rb # Module: nav_register, register_page, permissions API
lib/ruby_cms/engine.rb # Rails::Engine: config, initializers, rake tasks, nav registration
lib/ruby_cms/icons.rb # Named Heroicon SVG registry
lib/ruby_cms/dashboard_blocks.rb # Dashboard block registry
lib/ruby_cms/settings.rb # DB-backed settings
lib/ruby_cms/settings_registry.rb # Settings definitions and defaults
app/controllers/ruby_cms/admin/ # Admin controllers (base, dashboard, content blocks, etc.)
app/models/ # ContentBlock, ContentBlockVersion, Permission, etc.
app/views/ruby_cms/admin/ # Admin views
app/views/layouts/ruby_cms/ # Admin layout + sidebar
app/components/ # ViewComponents (AdminPage, etc.)
app/helpers/ # Content block, settings, dashboard helpers
| What | How |
|---|---|
| New admin page | rails g ruby_cms:admin_page <name> |
| New nav item | RubyCms.register_page(...) or RubyCms.nav_register(...) |
| New permission | RubyCms.register_permission_keys(:key) |
| New dashboard block | RubyCms.dashboard_register(...) |
| New setting | RubyCms::SettingsRegistry.register(...) |
| New icon | Pass raw SVG string to icon: parameter |
The gem is available as open source under the terms of the MIT License.