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

  1. Navigate to /railspress/admin/posts/new
  2. Enter a title (slug auto-generates)
  3. Write content using the rich text editor with support for bold, italic, headings, quotes, code blocks, lists, images, and links
  4. Select a category and add comma-separated tags
  5. Set status to "Published" when ready
  6. 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
![alt](url) 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

app/controllers/blog_controller.rb
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

config/routes.rb
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

app/views/blog/index.html.erb
<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

app/views/blog/show.html.erb
<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 Form Partial

app/views/blog/_search_form.html.erb
<%= 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:

app/controllers/blog_controller.rb
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

app/controllers/blog_controller.rb
def feed
  @posts = Railspress::Post.published
                           .includes(:category)
                           .ordered
                           .limit(20)

  respond_to do |format|
    format.rss { render layout: false }
  end
end

Route

config/routes.rb
get "blog/feed", to: "blog#feed", as: :blog_feed, defaults: { format: :rss }

RSS Builder View

app/views/blog/feed.rss.builder
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

app/helpers/blog_helper.rb
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:

app/views/blog/show.html.erb (top)
<% 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" %>