Resources
Perron is, just like Rails, designed with convention over configuration in mind.
All content is stored in app/content/*/*.{erb,md,*} and backed by a class, located in app/models/content/ that inherits from Perron::Resource.
The controllers are located in app/controllers/content/. To make them available, create a route: resources :posts, module: :content, only: %w[index show].
Resource class
Every resource class inherits from Perron::Resource, e.g.:
# app/models/content/post.rb
class Content::Post < Perron::Resource
end
This gives each class its base behavior. It is just a regular Ruby class, so all common methods are available:
# app/models/content/post.rb
class Content::Post < Perron::Resource
delegate :title, to: :metadata
def loud_category
metadata.category.upcase
end
end
Associations
Resources can be associated with each other using has_many and belongs_to, similar to ActiveRecord associations.
has_many
Define a has_many association to retrieve all resources that belong to the current resource:
# app/models/content/author.rb
class Content::Author < Perron::Resource
has_many :posts
end
<!-- app/content/authors/rails-designer.md -->
---
name: Rails Designer
bio: Creator of Perron
---
What could I say? I create lots of things? Like Perron, for example! 😊
Access the collection:
author = Content::Author.find("rails-designer")
author.posts # => Array of Content::Post instances where author_id matches
belongs_to
Define a belongs_to association when a resource references another resource:
# app/models/content/post.rb
class Content::Post < Perron::Resource
belongs_to :author
belongs_to :editor, class_name: "Content::Author"
end
In the content’s frontmatter, add the association name with _id suffix:
<!-- app/content/posts/my-first-post.md -->
---
title: My First Post
author_id: rails-designer
editor_id: cam
---
Post content here…
Access the associated resource:
post = Content::Post.find("my-first-post")
post.author # => Content::Author instance
post.editor # => Content::Author instance
Associations with Data sources
Associate resources with data sources using the class_name option. This works for both belongs_to and has_many associations.
belongs_to with Data
# app/models/content/post.rb
class Content::Post < Perron::Resource
belongs_to :editor, class_name: "Content::Data::Editors"
end
# app/content/data/editors.yml
- id: kendall
name: Kendall
bio: Master of the edit
- id: chris
name: Chris
bio: Editor Overlord
<!-- app/content/posts/my-first-post.md -->
---
title: My First Post
editor_id: kendall
---
Post content here…
The association works the same way as with Content resources, but pulls data from the structured data resource.
has_many with Data
For multiple associations, use an array of IDs in the frontmatter. This pattern mimics Rails’ has_and_belongs_to_many associations, where the relationship is defined by a list of IDs:
# app/models/content/post.rb
class Content::Post < Perron::Resource
has_many :editors, class_name: "Content::Data::Editors"
end
<!-- app/content/posts/my-first-post.md -->
---
title: My First Post
editor_ids: [kendall, chris]
---
Post content here…
Access the collection:
post = Content::Post.find("my-first-post")
post.editors # => Array of Content::Data::Editor instances where id matches editor_id
When {association_name}_ids is present in the frontmatter, Perron uses those IDs to find the associated records. If not present, it falls back to foreign key matching (e.g., looking for post_id in the data records).
Adjacency (next/previous)
Resources can navigate to adjacent resources (next/previous) in the collection:
<%= link_to "Previous", @resource.previous, rel: "prev" if @resource.previous %>
<%= link_to "Next", @resource.next, rel: "next" if @resource.next %>
By default, adjacency uses the collection’s default order (id). Configure your preferred ordering:
# app/models/content/post.rb
class Content::Post < Perron::Resource
adjacent_by :position
delegate :position, to: :metadata
end
You can also traverse cross-group by passing within:
class Content::Post < Perron::Resource
adjacent_by :position, within: :category
delegate :position, to: :metadata, :category
end
This assumes resources are grouped/connected through a category.
Or if categories are in a specific order, configure as:
class Content::Post < Perron::Resource
adjacent_by :position, within: { category: %w[getting_started content metadata] }
delegate :position, to: :metadata, :category
end
Validations
Validate values from resource classes, the frontmatter for example:
class Content::Post < Perron::Resource
CATEGORIES = %w[rails ruby hotwire javascript updates]
delegate :category, :title, :description, to: :metadata
validates :title, :description, presence: true
validates :category, inclusion: { in: CATEGORIES }
# …
end
Perron offers a bin/rails perron:validate task that runs all validations and outputs any failures. Useful to check if the title or meta description is within the correct range for SERP’s or to make sure the correct category is added. To validate frontmatter, make them callable in the class, as seen above using delegate.
Validation output could look something like this:
rails perron:validate
..........................F.....
Resource: /perron/docs/app/content/articles/resources.md
- Description can't be blank
Validation finished with 1 failure.
When using Rails 8.1, make Perron’s validate task part of the
bin/ciscript. Simply addstep "Perron: validate", "bin/rails perron:validate"within theCI.run doblock.
@resource instance
In a show action, a resource instance variable is expected to be set.
class Content::PostsController < ApplicationController
def show
@resource = Content::Post.find!(params[:id])
end
end
Various features from Perron rely on @resource instance variable being available.
Inline rendering
Use @resource.inline to render content without a view template. This is useful when your controller only needs to render the resource’s content:
class Content::PostsController < ApplicationController
def show
@resource = Content::Post.find(params[:id])
render inline: @resource.content
end
end
Setting a root page
To set a root page, create a root.{md,erb} file in the pages content directory (app/content/pages/root.erb) and add a root action in Content::PagesController:
# app/controllers/content/pages_controller.rb
class Content::PagesController < ApplicationController
def root
@resource = Content::Page.root
render :show
end
end
Then add the root route to config/routes.rb:
root to: "content/pages#root"
This is automatically generated when generating a Page collection using the content generator. Opt out by passing --no-include-root:
rails generate content Page --no-include-root
Custom collection name
When your route resource name doesn’t match your collection name, define a custom collection name:
# app/controllers/content/saas_posts_controller.rb
class Content::SaasPostsController < ApplicationController
# Route is `resources :saas_posts` but collection is "posts"
def self.collection_name = "posts"
def index
@resources = Content::Post.all
end
end
Perron checks for this method first before falling back to deriving the collection name from the controller name.
On this page