Building a Blog with RailsPress
Create a fully-featured blog with recent posts, categories, tags, search, RSS feeds, and SEO optimization.
Managing Content in the Admin
RailsPress provides a full admin interface at /railspress/admin. Here you can:
- Create Posts: Write blog posts with rich text content using the Lexxy editor
- Manage Categories: Organize posts into categories
- Manage Tags: Add tags for cross-cutting topics
Creating a Post
- Navigate to
/railspress/admin/posts/new - Enter a title (slug auto-generates)
- Write content using the rich text editor with support for bold, italic, headings, quotes, code blocks, lists, images, and links
- Select a category and add comma-separated tags
- Set status to "Published" when ready
- Click "Create Post"
Markdown Mode
The post editor supports switching between rich text and markdown editing modes. Click the toggle button in the editor toolbar to switch modes.
Switching to Markdown Mode
- Click the markdown toggle button in the editor toolbar
- Your rich text content is converted to markdown syntax
- Edit directly in markdown format
- Ideal for developers who prefer markdown
Switching Back to Rich Text
- Click the toggle button again
- Markdown is converted back to HTML
- Continue editing with the visual editor
Supported Markdown Syntax
| Syntax | Result |
|---|---|
# Heading |
H1 heading |
## Heading |
H2 heading |
**bold** |
Bold text |
*italic* |
Italic text |
~~strikethrough~~ |
Strikethrough |
`code` |
Inline code |
``` |
Code block |
> quote |
Blockquote |
- item |
Unordered list |
1. item |
Ordered list |
[text](url) |
Link |
 |
Image |
--- |
Horizontal rule |
Important Notes
- Content is automatically converted when you switch modes
- On form submission, markdown is converted to HTML for storage
- Complex HTML (custom styles, nested tables) may not convert cleanly to markdown
- The rich text editor preserves all formatting when switching back
Building the Frontend Blog
RailsPress provides models and data, but leaves frontend presentation to your application. This gives you full control over design and URL structure.
Step 1: Create a Blog Controller
class BlogController < ApplicationController
def index
@posts = Railspress::Post.published
.includes(:category, :tags)
.ordered
.page(params[:page])
.per(10)
end
def show
@post = Railspress::Post.published.find_by!(slug: params[:slug])
@related_posts = @post.category&.posts
&.published
&.where.not(id: @post.id)
&.ordered
&.limit(3) || []
end
def category
@category = Railspress::Category.find_by!(slug: params[:slug])
@posts = @category.posts
.published
.includes(:tags)
.ordered
.page(params[:page])
.per(10)
end
def tag
@tag = Railspress::Tag.find_by!(slug: params[:slug])
@posts = @tag.posts
.published
.includes(:category)
.ordered
.page(params[:page])
.per(10)
end
def search
@query = params[:q].to_s.strip
@posts = if @query.present?
Railspress::Post.published
.where("title ILIKE ? OR slug ILIKE ?", "%#{@query}%", "%#{@query}%")
.includes(:category, :tags)
.ordered
.page(params[:page])
.per(10)
else
Railspress::Post.none
end
end
end
Step 2: Add Routes
Rails.application.routes.draw do
# Mount RailsPress admin
mount Railspress::Engine => "/railspress"
# Blog frontend routes
get "blog", to: "blog#index", as: :blog
get "blog/search", to: "blog#search", as: :blog_search
get "blog/category/:slug", to: "blog#category", as: :blog_category
get "blog/tag/:slug", to: "blog#tag", as: :blog_tag
get "blog/:slug", to: "blog#show", as: :blog_post
end
Available Scopes
RailsPress provides these scopes out of the box:
| Scope | Description |
|---|---|
Railspress::Post.published |
Posts with status "published" and a published_at date |
Railspress::Post.drafts |
Posts with status "draft" |
Railspress::Post.ordered |
Posts ordered by created_at descending |
Railspress::Post.recent |
Last 10 posts (combines ordered with limit(10)) |
Recent Posts Feed
<div class="blog">
<h1>Blog</h1>
<%= render "blog/search_form" %>
<div class="posts">
<% @posts.each do |post| %>
<article class="post-card">
<h2>
<%= link_to post.title, blog_post_path(slug: post.slug) %>
</h2>
<div class="post-meta">
<time datetime="<%= post.published_at.iso8601 %>">
<%= post.published_at.strftime("%B %d, %Y") %>
</time>
<% if post.category %>
<span class="category">
<%= link_to post.category.name, blog_category_path(slug: post.category.slug) %>
</span>
<% end %>
</div>
<div class="post-excerpt">
<%= truncate(strip_tags(post.content.to_plain_text), length: 200) %>
</div>
<div class="post-tags">
<% post.tags.each do |tag| %>
<%= link_to tag.name, blog_tag_path(slug: tag.slug), class: "tag" %>
<% end %>
</div>
<%= link_to "Read more", blog_post_path(slug: post.slug), class: "read-more" %>
</article>
<% end %>
</div>
<%= render "blog/pagination", collection: @posts %>
</div>
Individual Post Pages
<article class="post">
<header class="post-header">
<h1><%= @post.title %></h1>
<div class="post-meta">
<time datetime="<%= @post.published_at.iso8601 %>">
<%= @post.published_at.strftime("%B %d, %Y") %>
</time>
<% if @post.category %>
<span class="category">
in <%= link_to @post.category.name, blog_category_path(slug: @post.category.slug) %>
</span>
<% end %>
</div>
</header>
<div class="post-content">
<%= @post.content %>
</div>
<footer class="post-footer">
<% if @post.tags.any? %>
<div class="post-tags">
<strong>Tags:</strong>
<% @post.tags.each do |tag| %>
<%= link_to tag.name, blog_tag_path(slug: tag.slug), class: "tag" %>
<% end %>
</div>
<% end %>
<nav class="post-navigation">
<%= link_to "Back to Blog", blog_path %>
</nav>
</footer>
</article>
<% if @related_posts.any? %>
<aside class="related-posts">
<h3>Related Posts</h3>
<ul>
<% @related_posts.each do |post| %>
<li>
<%= link_to post.title, blog_post_path(slug: post.slug) %>
<time><%= post.published_at.strftime("%b %d") %></time>
</li>
<% end %>
</ul>
</aside>
<% end %>
Category Pages
Category pages follow the same pattern as the index, but filtered by category. See the controller code above for the implementation.
Tag Pages
Tag pages work similarly to categories. Use the tag action in your controller.
Search Functionality
Search Form Partial
<%= form_with url: blog_search_path, method: :get, class: "search-form", data: { turbo_frame: "_top" } do |form| %>
<%= form.search_field :q,
value: @query,
placeholder: "Search posts...",
class: "search-input",
autofocus: @query.present? %>
<%= form.submit "Search", class: "search-button" %>
<% end %>
Full-Text Search (PostgreSQL)
For better search with PostgreSQL, update the search action to include ActionText content:
def search
@query = params[:q].to_s.strip
@posts = if @query.present?
Railspress::Post.published
.joins("LEFT JOIN action_text_rich_texts ON ...")
.where("railspress_posts.title ILIKE :q OR ...", q: "%#{@query}%")
.includes(:category, :tags)
.distinct
.ordered
.page(params[:page])
.per(10)
else
Railspress::Post.none
end
end
RSS Feed
Controller Action
def feed
@posts = Railspress::Post.published
.includes(:category)
.ordered
.limit(20)
respond_to do |format|
format.rss { render layout: false }
end
end
Route
get "blog/feed", to: "blog#feed", as: :blog_feed, defaults: { format: :rss }
RSS Builder View
xml.instruct! :xml, version: "1.0"
xml.rss version: "2.0", "xmlns:atom" => "..." do
xml.channel do
xml.title "Your Blog Name"
xml.description "Your blog description"
xml.link blog_url
xml.language "en"
@posts.each do |post|
xml.item do
xml.title post.title
xml.link blog_post_url(slug: post.slug)
xml.guid blog_post_url(slug: post.slug), isPermaLink: true
xml.pubDate post.published_at.rfc822
xml.description strip_tags(post.content.to_plain_text).truncate(300)
end
end
end
end
SEO Optimization
Meta Tags Helper
module BlogHelper
def blog_meta_title(post = nil)
if post
post.meta_title.presence || post.title
else
"Blog | Your Site Name"
end
end
def blog_meta_description(post = nil)
if post
post.meta_description.presence || truncate(strip_tags(post.content.to_plain_text), length: 160)
else
"Read our latest articles and updates."
end
end
end
Layout Head Section
Use content_for to set SEO metadata in your post views:
<% content_for :title, blog_meta_title(@post) %>
<% content_for :meta_description, blog_meta_description(@post) %>
<% content_for :canonical_url, blog_post_url(slug: @post.slug) %>
<% content_for :og_type, "article" %>