• Home
  • About
  • Portfolio
  • Contact
CodeCurious
  • Home
  • About
  • Portfolio
  • Contact
Go Back

How To Build A Blog With Ruby On Rails 8(Step-by-Step Guide)

Learn how to build a blog with Ruby on Rails 8 step by step. Real code, real examples, and beginner-friendly explanations.

Jean Emmanuel Cadet
By Jean Emmanuel Cadet • Ruby on Rails Developer

Last updated : Jun 13, 2026 • 15 min read

How to Build a Blog with Ruby on Rails 8(Step-by-Step Guide)

Last updated : Jun 13, 2026 • 15 min read

Share with friends

I remember scrolling through tutorial after tutorial, watching videos, reading documentation, and still feeling like I had never actually built anything that mattered.

I could copy code. I could follow along. But the moment someone said, "Now go build something on your own," I froze.

Then one evening, frustrated and a little tired, I decided to stop waiting for the perfect tutorial and just build a blog. Nothing fancy. Just posts. Titles. Content. A page where things actually showed up.

If you are at that same crossroads right now, this guide is for you. We are going to build a fully working blog together using Ruby on Rails 8, step by step, with real code and real explanations along the way.

Let us get started.


What You Will Build

By the end of this guide, you will have a working blog application that can:

  • Display a list of articles
  • Show a single article
  • Create new articles
  • Edit and delete existing articles

Simple. Focused. Real.


Prerequisites

Before we write a single line of code, make sure you have the following installed:

  • Ruby 3.2 or higher (follow the link to install it)
  • Rails 8.0 or higher (follow the link to install it)
  • SQLite3 (comes bundled with Rails by default)
  • A terminal you are comfortable using

To check your versions, run:

ruby -v
rails -v

If you see something like Ruby 3.3.0 and Rails 8.0.0, you are ready.


Step 1: Create Your Rails Application

Open your terminal and run:

rails new blog_app
cd blog_app

Rails will generate a complete project structure for you. You will see folders like app/, config/, db/, and more.

This is not magic. This is Rails giving you a well-organized home before you even write a line of your own code.

To verify everything works, start the server:

bin/rails server

Visit http://localhost:3000 in your browser. If you see the Rails welcome page, congratulations. Your app is alive.


Step 2: Understand the Structure Before You Touch It

Before we generate anything, take 60 seconds to look at what Rails created.

app/
controllers/ # Where your logic lives
models/ # Where your data lives
views/ # Where your pages live
config/
routes.rb # Where your URLs are defined
db/
schema.rb # Your database structure

If you want a deeper understanding of how Models, Views, and Controllers work together, read this: Rails MVC Tutorial for Beginners. It will make everything in this guide feel even more natural.


Step 3: Generate the Article Scaffold

Rails has a powerful generator that creates everything you need in one command. Run this:

bin/rails generate scaffold Article title:string body:text

Here is what just happened. Rails created:

  • A migration file to add an articles table to your database
  • An Article model
  • An ArticlesController with all CRUD actions
  • Views for listing, showing, creating, and editing articles
  • Routes for your article URLs

Now run the migration to apply the database changes:

bin/rails db:migrate

Start your server again and visit http://localhost:3000/articles.

You now have a working blog. But we are not done. Let us understand what was built and then make it better.


Step 4: Look at Your Routes

Open config/routes.rb. You will see:

Rails.application.routes.draw do
resources :articles
end

This single line creates seven routes for you. Run this command to see them all:

bin/rails routes

You will see routes like:

GET    /articles          articles#index
GET /articles/:id articles#show
GET /articles/new articles#new
POST /articles articles#create
GET /articles/:id/edit articles#edit
PATCH /articles/:id articles#update
DELETE /articles/:id articles#destroy

Seven routes. One line. That is Rails working for you.

Let us also set the home page of the application to the articles list. Update your routes.rb:

Rails.application.routes.draw do
root "articles#index"
resources :articles
end

Now visiting http://localhost:3000 will take you directly to your articles list.


Step 5: Explore the Controller

Open app/controllers/articles_controller.rb. You will find all seven actions already written for you.

class ArticlesController < ApplicationController
before_action :set_article, only: %i[ show edit update destroy ]

# GET /articles or /articles.json
def index
@articles = Article.all
end

# GET /articles/1 or /articles/1.json
def show
end

# GET /articles/new
def new
@article = Article.new
end

# GET /articles/1/edit
def edit
end

# POST /articles or /articles.json
def create
@article = Article.new(article_params)

respond_to do |format|
if @article.save
format.html { redirect_to @article, notice: "Article was successfully created." }
format.json { render :show, status: :created, location: @article }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @article.errors, status: :unprocessable_entity }
end
end
end

# PATCH/PUT /articles/1 or /articles/1.json
def update
respond_to do |format|
if @article.update(article_params)
format.html { redirect_to @article, notice: "Article was successfully updated.", status: :see_other }
format.json { render :show, status: :ok, location: @article }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @article.errors, status: :unprocessable_entity }
end
end
end

# DELETE /articles/1 or /articles/1.json
def destroy
@article.destroy!

respond_to do |format|
format.html { redirect_to articles_path, notice: "Article was successfully destroyed.", status: :see_other }
format.json { head :no_content }
end
end

private
# Use callbacks to share common setup or constraints between actions.
def set_article
@article = Article.find(params.expect(:id))
end

# Only allow a list of trusted parameters through.
def article_params
params.expect(article: [ :title, :body ])
end
end

Notice how clean this is. Each action has one job. The controller coordinates. It does not cook the food, it just takes the order and delivers the result. Exactly like the waiter analogy you may have read about before.


Step 6: Add Validations to Your Model

Right now, someone could create an article with no title and no content. That is not a great experience.

Open app/models/article.rb and add:

class Article < ApplicationRecord
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true, length: { minimum: 10 }
end

Now try creating an empty article. Rails will stop you and display the validation errors automatically because the scaffold already handles this in the views.

Your data is now protected. That is your Model doing its job as a guardian.


Step 7: Make the Views Feel Like Yours

Rails generated views for you, but let us look at the most important ones and understand what they do.

Open app/views/articles/index.html.erb:

<h1>Articles</h1>

<% @articles.each do |article| %>
<article>
<h2><%= link_to article.title, article %></h2>
<p><%= truncate(article.body, length: 100) %></p>
<%= link_to "Read more", article %>
</article>
<% end %>

<%= link_to "New Article", new_article_path, class: "btn btn-primary" %>

This is ERB, which stands for Embedded Ruby. The <% %> tags execute Ruby code. The <%= %> tags execute and output the result to the page.

Notice @articles comes from the controller. The view never touches the database directly. That separation is intentional and powerful.


Step 8: Add a Published Status

Let us add a real feature: the ability to publish or draft an article.

Generate a new migration:

bin/rails generate migration AddPublishedToArticles published:boolean

Open the generated migration file in db/migrate/ and update it to set a default value:

class AddPublishedToArticles < ActiveRecord::Migration[8.0]
def change
add_column :articles, :published, :boolean, default: false, null: false
end
end

Run the migration:

bin/rails db:migrate

Now update the model to use this field:

class Article < ApplicationRecord
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true, length: { minimum: 10 }

scope :published, -> { where(published: true) }
scope :drafts, -> { where(published: false) }
end

And update the index action in your controller only to show published articles:

def index
@articles = Article.published
end

Now only articles with published: true will appear on your blog index page. Drafts stay hidden until you are ready.

But wait. If you try to create an article right now and check the published checkbox, nothing will actually get saved. Rails has a security layer called strong parameters, and it will silently ignore any field you have not explicitly allowed. Let us fix that.


Step 8.1: Permit the Published Param in the Controller

Open app/controllers/articles_controller.rb and find the article_params method at the bottom. It currently looks like this:

def article_params
params.expect(article: [ :title, :body ])
end

Update it to include :published:

def article_params
params.expect(article: [ :title, :body, :published ])
end

Notice the use of params.expect here. This is a cleaner Rails 8 syntax that replaces the older require(...).permit(...) pattern. It is more explicit and reads naturally: "I expect an article with these fields."

Without this change, even if a user checks the published checkbox in the form, Rails will strip the value out before saving. The column exists in the database, but the controller was never letting it through. Now it will.


Step 8.2: Add the Published Checkbox to the Form

The form that Rails generated for creating and editing articles lives in app/views/articles/_form.html.erb. This is a partial, which means it is shared between the new and edit pages.

Open it and add the following block before the submit button:

<div>
<%= form.label :published, style: "display: block" %>
<%= form.checkbox :published %>
</div>

This renders a labeled checkbox that is bound directly to the published attribute on the article. When the user checks it and submits the form, Rails will receive published: true. When left unchecked, it will pass published: false.

Combined with the scope you added to the model and the updated index action in the controller, the full flow now works end to end:

  1. A user creates an article and checks the published checkbox
  2. The form submits published: true
  3. The controller permits it through article_params
  4. Rails saves it to the database
  5. The index action fetches only Article.published
  6. The article appears on the blog

That is the complete published/draft system, fully wired up.


Step 9: Watch Out for N+1 Queries

This is where many developers hit a wall they did not see coming.

Imagine your articles now belong to authors. You might write:

def index
@articles = Article.published
end

And in the view:

<% @articles.each do |article| %>
<%= article.author.name %>
<% end %>

This looks harmless. But Rails is firing one database query for every single article to load the author. With 50 articles, that is 51 queries. With 500, it is 501.

The fix is simple:

def index
@articles = Article.published.includes(:author)
end

This is called eager loading. It tells Rails to fetch all the authors in one query instead of one per article.

If you want to go deep on this topic and learn how to spot and fix these issues like a pro, read this: Fix N+1 Queries in Rails: A Developer's Journey to Speed.

It could save your app from a performance disaster the moment traffic picks up.


Step 10: Add Basic Styling

Rails 8 ships with a minimal setup that works great out of the box. You can add a CSS file to make your blog look a bit friendlier.

Open app/assets/stylesheets/application.css and add:

body {
font-family: Georgia, serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background-color: #fafafa;
color: #222;
}

h1, h2 {
color: #1a1a2e;
}

a {
color: #4a90d9;
text-decoration: none;
}

article a:hover {
text-decoration: underline;
}

article {
margin-bottom: 2rem;
}

strong {
display: block;
margin-bottom: .2rem;
}

.btn {
padding: .5rem 1rem;
font-size: 1rem;
border-radius: .2rem;
color: #fff;
cursor: pointer;
}

input, textarea {
border: 1px solid #ddd;
border-radius: .2rem;
}

.form-label {
display: block;
margin-bottom: .5rem;
}

.form-control {
display: block;
width: 100%;
padding: .375rem .75rem;
font-size: 1rem;
color: #212529;
background-color: #fff;
border: 1px solid #dee2e6;
}

.input-group {
margin-bottom: 1rem;
}

Step 10.1: Update the Form Partial to Use the New Classes

The CSS classes you just wrote will do nothing until the form knows about them. Open app/views/articles/_form.html.erb and replace everything inside it with:

<%= form_with(model: article) do |form| %>
<% if article.errors.any? %>
<div style="color: red">
<h2><%= pluralize(article.errors.count, "error") %> prohibited this article from being saved:</h2>

<ul>
<% article.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="input-group">
<%= form.label :title, class: "form-label" %>
<%= form.text_field :title, class: "form-control" %>
</div>

<div class="input-group">
<%= form.label :body, class: "form-label" %>
<%= form.textarea :body, rows: 3, class: "form-control" %>
</div>

<div class="input-group">
<%= form.label :published %>
<%= form.checkbox :published %>
</div>

<div>
<%= form.submit class: "btn btn-primary" %>
</div>
<% end %>

Here is what changed and why it matters.

Each field is now wrapped in a div with the class input-group, which gives it consistent bottom spacing so the fields breathe. The title and body labels carry the class form-label, which makes them display as block elements with a small margin below, so the label sits cleanly above its input. The inputs themselves carry form-control, which adds padding, a readable font size, and a subtle border. The submit button has the btn class, which gives it a blue background with a hover effect.

The published checkbox keeps its simpler layout since checkboxes sit inline next to their label and do not need the full form-control treatment.

When you refresh the new article page now, the form will look polished and intentional instead of raw browser defaults. Small details like these are what make a project feel finished rather than just functional.

Not fancy. But clean. And yours.


Step 10.2: Style the Show Page

The show page is what your readers see when they click on an article. Right now, it uses the default scaffold output, which gets the job done but does not match the style you just built.

Open app/views/articles/show.html.erb and replace its contents with:

<p style="color: green"><%= notice %></p>

<%= render @article %>

<div>
<%= link_to "Edit this article", edit_article_path(@article), class: "btn btn-primary" %> |
<%= link_to "Back to articles", articles_path %>

<%= button_to "Destroy this article", @article, method: :delete, class: "btn btn-danger" %>
</div>

A few things worth noticing here. The flash notice at the top uses an inline green color to confirm successful actions like creating or updating an article. The render @article line is Rails shorthand that automatically looks for the _article.html.erb partial and renders it with the current article, which we will set up in the next step.

The Edit link carries the btn btn-primary class, and the Destroy button carries the btn btn-danger. You will need to add those two variants to your CSS. Open app/assets/stylesheets/application.css and add these after your existing .btn rules:

.btn-primary {
background-color: #4a90d9;
color: #fff;
}

.btn-primary:hover {
background-color: rgba(74, 144, 217, 0.95);
color: #fff;
}

.btn-danger {
background-color: #d9534f;
color: #fff;
border: none;
padding: .5rem 1rem;
margin-top: 1rem;
border-radius: .2rem;
cursor: pointer;
}

.btn-danger:hover {
background-color: rgba(217, 83, 79, 0.9);
}

This gives your Edit action a calm blue and your Destroy button a clear red, so readers and editors can always tell at a glance which action is which. Good design communicates intent before anyone reads a word.


Step 10.3: Style the Article Partial

The _article.html.erb partial is the piece that renders the actual article content on both the show page and anywhere else you want to reuse it. Right now, it probably shows raw field labels with no visual structure.

Open app/views/articles/_article.html.erb and replace its contents with:

<div id="<%= dom_id article %>">
<div class="article-detail">
<strong>Title:</strong>
<%= article.title %>
</div>

<div class="article-detail">
<strong>Body:</strong>
<%= article.body %>
</div>
</div>

The dom_id helper generates a unique HTML id for each article, like article_1 or article_5. This is useful if you ever want to target a specific article with JavaScript or CSS later.

Each field is wrapped in a div with the class article-detail. Add that class to your stylesheet:

.article-detail {
margin-bottom: 1rem;
line-height: 1.6;
}

This adds breathing room between the title and body, and the line-height makes the body text easier to read at a glance.

With these two steps done, your blog now has a consistent visual language from the list page all the way through to the individual article view. Every page feels like part of the same thing, because it is.


Step 11: Test Your Blog End to End

Let us do a quick walkthrough to make sure everything works.

Start the server:

bin/rails server

Visit http://localhost:3000/articles/new and create a new article. Set published to true.

Go to http://localhost:3000. Your article should appear.

Click on it. The show page should display the full content.

Try editing it. Try deleting it.

If everything works, you just built a blog from scratch.


What You Just Accomplished

Let us take a breath and look at what you built:

  • A Rails 8 application with a real database
  • Full CRUD for articles
  • Validation to protect your data
  • A published/draft system
  • Awareness of performance with eager loading
  • A working, styled blog you can show people

That is not a tutorial exercise. That is a foundation you can grow.


Where to Go From Here

Your blog works. But good developers keep pushing. Here are your next steps:

  • Add user authentication with Devise
  • Add categories or tags to your articles
  • Add pagination with Pagy or Kaminari
  • Deploy to Hetzner, Render, or Fly.io, so the world can see it
  • Add ActionText for rich content editing

Every one of those features will teach you something new. Every one of them is a skill employers look for.


Source Code

The complete source code for this project is available on GitHub. If you got stuck at any point, want to compare your work, or just want to clone it and run it locally, it is all there waiting for you.

github.com/jecode93/build-a-blog-with-ruby-on-rails-8

Feel free to fork it, explore it, and make it your own.

The most important thing you can do right now is not find the next tutorial. It is to open your terminal, run rails new, and build something else.

Because the developers who grow fastest are not the ones who read the most. They are the ones who build the most.

So keep building. Keep pushing. Keep going.

💌 Don’t miss out! Join my newsletter for web development tips, tutorials, and insights delivered straight to your inbox.

Thanks for reading & Happy coding! 🚀

Follow me on:

Code. Learn. Grow.

A friendly newsletter sharing dev tips, lessons, and wins from my journey.

    Services Tailored to Your Needs


    coding

    Web & Mobile Development

    Custom websites and mobile apps built to be fast, modern, and user-friendly. From sleek landing pages to full-scale applications, I deliver solutions that engage your audience and grow your business.

    API development

    Seamlessly connect your systems with secure, scalable APIs. I design and integrate APIs that improve efficiency, reliability, and flexibility for your business processes.

    Database design and management

    Reliable database solutions tailored to your needs. I design, optimize, and maintain databases that ensure performance, security, and scalability for your applications.

    You might also like…

    Git Aliases That Boost Your Coding Speed by 10x
    Learning Concepts

    Git Aliases That Boost Your Coding Speed By 10x

    By Jean Emmanuel Cadet
    Published on: Nov 17, 2025
    Design Systems Explained: The Beginner's Complete Guide
    Design & UX

    Design Systems Explained: The Beginner's Complete Guide

    By Jean Emmanuel Cadet
    Published on: May 02, 2025
    Fix N+1 Queries in Rails: A Developer's Journey to Speed
    Web Development

    Fix N+1 Queries In Rails: A Developer's Journey To Speed

    By Jean Emmanuel Cadet
    Published on: Jan 26, 2026
    CodeCurious

    Designed for those who view software as architecture and code as literature.

    Legal
    Terms & Conditions Privacy Policy Disclaimer

    CodeCurious © 2025 - 2026. All rights reserved. | Made with ♥ by @jecode93