Iron
Iron is a flexible CMS engine for Rails apps
Usage
Publishing Content to the Web
Iron CMS allows you to publish content types to your website. To set this up:
- Install the pages controller in your application:
bin/rails g iron:pages
This will:
- Create a
PagesControllerin your app - Add the
iron_pagesroute to your routes file - Create a default view template
- Create custom templates for your content types:
bin/rails g iron:template article
This creates a template at app/views/templates/article.html.erb that you can customize.
Enable web publishing for a content type in the Iron admin panel:
- Go to Content Types
- Edit your content type
- Enable "Web Published"
- Set a base path (optional)
Access your content on the web:
- Entries will be available at their generated routes
- Use the
iron_entry_path(@entry.attributes)helper to generate URLs - Multi-locale support is built-in with URL prefixes
Responsive Images
Iron provides helpers for efficient, responsive image rendering with automatic variant generation and modern format support.
Setup
Include the helper in your application:
# app/helpers/application_helper.rb
module ApplicationHelper
include Iron::ImageHelper
end
Usage
Use iron_picture_tag for optimal image delivery:
<%= iron_picture_tag field.file %>
This automatically:
- Generates responsive variants (150w, 320w, 640w, 1024w, 1920w)
- Serves modern formats (AVIF, WebP) with automatic fallback
- Adds proper dimensions to prevent layout shift
- Enables lazy loading by default
- Displays blur placeholders during image load
Customization
Specify responsive sizes based on your layout:
<%= iron_picture_tag field.file,
sizes: "(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw",
class: "rounded-lg",
loading: "eager" %>
Disable blur placeholders when not needed:
<%= iron_picture_tag field.file, blur: false %>
Alternative: Simple Image Tag
For cases where you need a basic responsive image without format optimization:
<%= iron_image_tag field.file %>
This generates a standard <img> with srcset but without modern format variants.
Development Seeding
Iron provides a seeding mechanism to snapshot and restore CMS schema and content, making it easy for teammates to get a consistent development database.
Creating a seed
After setting up your content types and sample content via the admin UI:
bin/rails iron:seed:dump
This exports the full CMS state to db/seeds/iron.zip. Commit this file to your repository.
Loading a seed
Add this to your db/seeds.rb:
Iron::Seed.load
Then rails db:prepare (or rails db:seed) will automatically bootstrap the CMS when the database is empty. Loading is skipped if content types already exist.
Admin credentials
The seed creates a default admin user during loading. Configure via environment variables:
IRON_SEED_EMAIL(default:admin@example.com)IRON_SEED_PASSWORD(default:password)
Building with AI Agents
Iron is built for agent-driven development: the CMS schema lives in a declarative file and content is managed through rake tasks, so a coding agent can take a site from brief to working CMS without clicking through the admin.
Install the knowledge package:
bin/rails g iron:agents
This creates .claude/skills/iron-cms/SKILL.md — a Claude Code skill teaching agents the full workflow (schema file format, field types, payload formats) — and adds an ## Iron CMS command crib sheet to your AGENTS.md (created if missing).
The schema lives in db/cms/schema.json and declares locales, block definitions and content types:
bin/rails iron:schema:dump # write the current schema to db/cms/schema.json
bin/rails iron:schema:diff # preview what apply would change
bin/rails iron:schema:apply # upsert the file into the database (PRUNE=1 also deletes)
Apply upserts by handle and is additive by default. In development, schema edits made in the admin UI are dumped back to the file automatically, so it stays current either way.
Content is managed imperatively:
bin/rails iron:content:list HANDLE=post
bin/rails iron:content:get HANDLE=post ROUTE=hello-world
bin/rails iron:content:create HANDLE=post PAYLOAD='{"title": "Hello"}'
bin/rails iron:content:update HANDLE=post ID=12 PAYLOAD='{"title": "Hi"}'
bin/rails iron:content:delete HANDLE=post ID=12
Payloads are JSON objects of field handles to values — inline via PAYLOAD=, from a file via FILE=, or piped on stdin. Results print as JSON on stdout; validation errors as JSON on stderr. See the generated skill for payload semantics per field type, file ingestion, and the end-to-end loop.
System Requirements
Iron requires libvips to be installed on your system for image processing and responsive image generation.
macOS:
brew install vips
Ubuntu/Debian:
sudo apt-get install libvips
Arch Linux:
sudo pacman -S libvips
Note: Rails-generated production Dockerfiles typically include libvips by default, so no additional Docker configuration is needed.
Installation
Quickstart
Create a new Iron-powered site in two commands:
gem install iron-cms
iron new mysite
iron new scaffolds a fresh Rails app and runs rails g iron:install to wire Iron in. Then start it:
cd mysite
bin/dev # open http://localhost:3000/admin
When developing Iron itself, point the new app at a local checkout with iron new mysite --path /path/to/iron.
For an existing Rails app, add the gem and run the generator directly:
bundle add iron-cms
bin/rails g iron:install
rails g iron:install wires everything up and is safe to re-run:
- mounts
Iron::Engineat/admin - installs the Active Storage, Action Text, and Iron migrations
- drops a minimal
db/cms/schema.jsonskeleton for the schema-as-code workflow - appends an idempotent administrator-provisioning block to
db/seeds.rb(the SKILL.md seed-zip workflow addsIron::Seed.loadbelow this block rather than re-provisioning the admin) - emits
lib/tasks/iron_release.rakesodb:preparealso runsiron:schema:apply - installs the
iron-cmsagent skill (rails g iron:agents) and the pages controller (rails g iron:pages) - runs
db:preparethendb:seed(migrate, apply the schema, and provision the first admin)
Provisioning credentials come from IRON_SEED_EMAIL / IRON_SEED_PASSWORD (defaulting to admin@example.com / password in local environments — development and test; required in any non-local environment like staging or production, where seeding fails fast without them).
The local agent workflow drives Iron through the iron:schema / iron:content rake tasks and needs no API token. If you later want to reach the REST API over HTTP, mint one explicitly with bin/rails iron:integration:create NAME=agent ROLE=administrator (it's shown once).
Because db:prepare now also applies the CMS schema, bin/kamal deploy converges migrations and the CMS schema on every deploy — its Docker entrypoint runs db:prepare, so no deploy-specific wiring is needed. The hook works under any deploy mechanism that runs db:prepare.
The iron-cms agent skill and an ## Iron CMS section in AGENTS.md are now installed: open the new app in Claude Code and hand it a content brief — it will model db/cms/schema.json, apply it, and populate content using the iron:schema / iron:content tasks.
Manual installation
If you prefer to wire Iron up by hand, add this line to your application's Gemfile:
gem "iron-cms"
And then execute:
bundle
Or install it yourself as:
gem install iron-cms
Iron stores file uploads with Active Storage and rich text with Action Text. If your application doesn't have their tables yet, install them first, then copy the Iron migrations and migrate:
bin/rails active_storage:install
bin/rails action_text:install:migrations
bin/rails iron:install:migrations
bin/rails db:migrate
(action_text:install:migrations copies just the table migration — Iron ships its own editor, so the full Action Text JavaScript setup isn't needed.)
Configure the mailer sender address used for system emails (e.g. password resets):
# config/environments/production.rb
config.action_mailer. = { from: "hello@mysite.com" }
Architecture
Here is an overview of how the CMS is structured.
erDiagram
ENTRY }o--|| CONTENT_TYPE : "conforms to"
CONTENT_TYPE ||--o{ FIELD_DEFINITION : "defines"
CONTENT_TYPE |o--o| FIELD_DEFINITION : "title"
BLOCK_DEFINITION |o--o{ FIELD_DEFINITION : "defines"
FIELD_DEFINITION }o--o{ BLOCK_DEFINITION : "supports"
ENTRY ||--o{ FIELD : "has"
FIELD }o--|| LOCALE : "has"
FIELD_DEFINITION ||--o{ FIELD : "defined by"
BLOCK_DEFINITION |o--o{ FIELD : "defines"
FIELD |o--o{ FIELD : "has"
ENTRY {
}
CONTENT_TYPE {
string name "Content type name"
string handle "Used for the API"
string description "Description for documentation purposes"
}
FIELD_DEFINITION {
string name "Used as display name in CMS"
string handle "Used for the API"
string type "Definition type: text | number | file | block_definition"
boolean localized "If the definition supports multiple locales"
}
BLOCK_DEFINITION {
string name "block name"
string handle "Used for the API"
string description "Description for documentation purposes"
}
FIELD {
string type "Field type: text | number | block"
string value_text "Text value (optional)"
decimal value_number "Number value (optional)"
}
LOCALE {
string code "Locale code"
string name "Locale name"
}
Publishing
To release a new version of the Iron gem:
Update the version and changelog:
- Update the version number in
lib/iron/version.rb - Update
CHANGELOG.mdwith the changes for the new version - Run
bundle installto update the lock file with the new version
- Update the version number in
Commit the changes:
git commit -am "your commit message"
- Release the gem:
bin/rails release
This command will build the gem, tag the commit with the version number, push to GitHub, and publish to RubyGems.
Contributing
Contribution directions go here.
License
The gem is available as open source under the terms of the MIT License.