Build a social feed with Ruby on Rails and PostgreSQL
To follow this tutorial, you will need Ruby and Rails installed on your machine. A basic understanding of Ruby, PostgreSQL and CoffeeScript will help you get the most out of this tutorial.
Introduction
The common feature at the heart of every social media platform is posts. In one way or another, every social media platform out there provides a summary of these posts in feeds.
The best social media platforms offer realtime updates of posts in user feeds. This way, no social content is delayed and everyone gets access to the latest information as soon as it drops.
In this post, we’ll build a simple app with realtime feed and likes. Posts will appear in our feed as soon as they are posted as well as their like counts increase in realtime once they’re liked.
A sneak-peek into what we will build in this post:
Prerequisites
A basic understanding of Ruby, CoffeeScript and PostgreSQL will help you get the best out of this tutorial. You can check the PostgreSQL, Ruby and Rails documentation for installation steps.
Setting up the application
Before starting off, ensure that you have Ruby and Rails installed. Run the following command to confirm your version:
$ ruby -v // 2.1 or above
$ rails -v // 4.2 or above
Open your terminal and run the following Rails commands to create our demo application:
# create a new Rails application
$ rails new pusher-live-feeds -T --database=postgresql
Go ahead and change directory into the newly created pusher-live-feeds
folder:
# change directory
$ cd pusher-live-feeds
In the root of your pusher-live-feeds
directory, open your Gemfile
and add the following gems:
# Gemfile
gem 'bootstrap', '~> 4.1.0'
gem 'jquery-rails'
gem 'pusher'
gem 'figaro'
In your terminal, ensure you are in the pusher-live-feeds
project directory and install the gems by running:
$ bundle install
Database setup
To get our app up and running, we’ll go ahead and create a database for it to work with. You can check out this article on how to create a Postgres database and an associated user and password.
Once you have your database details, in your database.yml
file, under the development
key, add the following code:
# config/database.yml
...
development:
<<: *default
database: pusher-live-feeds_development // add this line if it isn't already there
username: database_user // add this line
password: user_password // add this line
...
Ensure that the username and password entered in the code above has access to the pusher-live-feeds_development database
. After that, run the following code to setup the database:
# setup database
$ rails db:setup
Bootstrap the application
With our database all set up, we’ll go ahead and create our models and controllers. In your terminal, while in the project’s directory, run the following code:
# generate a post model
$ rails g model post username:string post:text
# generate a like model
$ rails g model like like_count:integer post:references
# generate a posts controller with the index, new and create view
$ rails g controller posts index new create
Next, we’ll modify our like model migration file to contain an initial default value for likes. In the db/migrate
folder, look for the create likes migration file. It should be a file with the current date stamp and ends with _create_likes.rb
. In that file, update the code there with the following:
# db/migrate/20180520125755_create_likes.rb
class CreateLikes < ActiveRecord::Migration[5.1]
def change
create_table :likes do |t|
t.integer :like_count, default: 0 # add the default: 0 part.
t.references :post, foreign_key: true
t.timestamps
end
end
end
In our post model, we’ll also add an association to the likes model. In your post model, add the following code:
# app/models/post.rb
class Post < ApplicationRecord
has_many :likes
end
Now, we’re ready to run our database migrations and see our new app. In your terminal, run the following code:
# run database migrations
$ rails db:migrate
After running migrations, start the development server on your terminal by running rails s
. Visit http://localhost:3000 in your browser to see your brand new application:
Pusher account setup
It’s time for us to create our app on Pusher. Head over to Pusher and sign up for a free account.
Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:
Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with for a better setup experience:
Click the App Keys tab to retrieve your keys
Building the homepage
With our Pusher account set up, let’s change our app’s landing page to something eye-worthy. Let’s set our homepage to our post’s index page and add the necessary routes for our app.
In your routes file, add the following code:
# config/routes.rb
Rails.application.routes.draw do
resources :posts
post '/likes/:post_id', to: 'posts#add_like', as: "add_likes"
root 'posts#index'
end
Next, we’ll require Bootstrap and add some styling. Add the following code to your application.js
file, all before the last line:
# app/assets/javascripts/application.js
.....
//= require jquery3 # add this line
//= require popper # add this line
//= require bootstrap # add this line
//= require_tree .
Rename your application.css
file to application.scss
and add the following code:
# app/assets/stylesheets/application.scss
@import "bootstrap";
@import url('https://fonts.googleapis.com/css?family=Tajawal');
body {
font-family: 'Tajawal', sans-serif;
}
#post {
min-height: 5rem;
max-height: 8rem;
}
.card-header {
padding: 0.25rem 0.85rem;
font-weight: 700;
}
.card-body {
padding: 0.55rem 0.85rem;
}
.far {
cursor: pointer;
}
Now, we’ll add the HTML markup for our homepage in our index.html.erb
file:
# app/views/posts/index.html.erb
<div class="container-fluid">
<div class="container">
<div class="container bg-light p-3 col-8 col-lg-6 welcome-page">
<h5 class="text-center">Enter your username</h5>
<input type="text" id="new-user-form" class="form-control my-5" required />
</div>
<div class="container bg-light p-3 col-8 col-lg-6 post-page collapse">
<div class="post-form-wrapper">
<p class="current-user"></p>
<%= form_with(model: @post, scope: :post, format: :json, id: 'post-form') do |form| %>
<div class="field">
<%= form.text_area :post, id: :post, class: "form-control post-textarea", required: true %>
<%= form.hidden_field :username, id: :username %>
</div>
<div class="actions text-right">
<%= form.submit 'Submit post', class: "btn btn-success btn-sm mt-1" %>
</div>
<% end %>
</div>
<div class="posts mt-5">
<% @posts.each do |post| %>
<div class="post-wrapper col-12 mb-2 p-0">
<div class="card">
<div class="card-header">
@<%= post.username %>
<small class="float-right mt-1"><%= post.created_at.strftime("at %I:%M%p") %></small>
</div>
<div class="card-body">
<p class="card-text"><%= post.post %></p>
<%= link_to '', add_likes_path(post_id: post.id), remote: true, method: :post, class: "far fa-thumbs-up add-like" %><span class="ml-2" data-post="<%= post.id %>"><%= post.likes[0].like_count %></span>
</div>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
Next, we’ll add the JavaScript code to display the feeds page after collecting the user’s username. Add the following code to your post.coffee
file:
# app/assets/javascripts/post.coffee
$(document).ready =>
currentUser = ''
welcomePage = $('.welcome-page')
postPage = $('.post-page')
newUserForm = $('#new-user-form')
# when user enters a username, store it and show the post page
newUserForm.on 'keyup', (event) ->
if event.keyCode == 13 and !event.shiftKey
currentUser = event.target.value
newUserForm.val('')
welcomePage.addClass('collapse')
postPage.removeClass('collapse')
greeting = """welcome @#{currentUser}"""
$('.current-user').html(greeting)
$('#username').val(currentUser)
return
In our posts controller, we’ll instantiate a posts object. In your posts_controller.rb
file, add the following code:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.all.order(created_at: :desc) # add this line
end
...
end
Lastly, we’ll be making use of Font Awesome icons, so we need to add the Font Awesome CDN to our app head tag. While we’re at it, we’ll also add the Pusher library.
# app/views/layouts/application.html.erb
<head>
....
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp" crossorigin="anonymous"> # add this line
<script src="https://js.pusher.com/4.1/pusher.min.js"></script> # add this line
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
If you’ve followed the tutorial so far, when you reload the homepage, you should see the first image below. On entering a username, you should see the second image below.
If you encounter a RegExp error while trying to set up Bootstrap, In config/boot.rb
, change the ExecJS runtime from Duktape to Node.
# config/boot.rb
ENV['EXECJS_RUNTIME'] ='Node'
Adding posts
Now that our app’s UI is set up, we’ll go ahead and start adding posts. In the posts controller, we’ll add code for creating posts and liking them. Update your posts controller with following:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.all.order(created_at: :desc)
end
def new
@post = Post.new
end
def create
@post = Post.new(post_params)
@post.likes.build()
respond_to do |format|
if @post.save
format.json { render :show, status: :created }
else
format.json { render json: @chat.errors, status: :unprocessable_entity }
end
end
end
def add_like
@post = Post.find(params[:post_id])
if @post
@post.likes[0].like_count +=1
if @post.likes[0].save
respond_to do |format|
format.json { render :show, status: :ok }
end
end
end
end
private
def post_params
params.require(:post).permit(:post, :username)
end
end
We’ll build our server response to JSON using Jbuilder. In your posts views folder, create a show.json.jbuilder
file and add the following code:
# app/views/posts/show.json.jbuilder
json.extract! @post, :id, :username, :post, :created_at
json.url post_url(@post, format: :json)
json.likes @post.likes[0].like_count
Whenever a new post is created, we’ll handle it via AJAX and prepend it to our current feed so the new feeds are at the top. Update your posts.coffee
file with the following:
# app/assets/javascripts/posts.coffee
# function for adding new posts to the feed
updateFeed = (post) ->
postTime = new Date(post.created_at.replace(' ', 'T')).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })
$('.posts').prepend """
<div class="post-wrapper col-12 mb-2 p-0">
<div class="card">
<div class="card-header">@#{post.username}<small class="float-right mt-1">at #{postTime}</small></div>
<div class="card-body">
<p class="card-text">#{post.post}</p>
<a class="far fa-thumbs-up add-like" data-remote="true" rel="nofollow" data-method="post" href="/likes/#{post.id}"></a>
<span class="ml-2" data-post="#{post.id}">#{post.likes[0].like_count}</span>
</div>
</div>
</div>
"""
return
# if the post was successfully saved, get the post and pass it to the updateFeed function
$('#post-form').on 'ajax:success', (data) ->
post = data.detail[0]
updateFeed post
$('#post-form')[0].reset()
return
With that, we should be able to create new posts and see them appear in our feed. Next, we’ll add our killer realtime feature.
Realtime feed with Pusher
To make our feed realtime, whenever a new post is created, we publish it on the server via Pusher and subscribe to it on the frontend of our app. Before we can do this though, we need to initialize our Pusher client.
In the config/initializers
folder, create a pusher.rb
file and add the following code:
# config/initializers/pusher.rb
require 'pusher'
Pusher.app_id = ENV["PUSHER_APP_ID"]
Pusher.key = ENV["PUSHER_KEY"]
Pusher.secret = ENV["PUSHER_SECRET"]
Pusher.cluster = ENV["PUSHER_CLUSTER"]
Pusher.logger = Rails.logger
Pusher.encrypted = true
Next, install Figaro by running figaro install
in your terminal. It will generate an application.yml
file. In the application.yml
file add your Pusher keys:
# config/application.yml
PUSHER_APP_ID: 'xxxxxx'
PUSHER_KEY: 'xxxxxxxxxxxxxxxxx'
PUSHER_SECRET: 'xxxxxxxxxxxxxx'
PUSHER_CLUSTER: 'xx'
Now we can go ahead and publish new posts and likes whenever they’re created. Add the following code to your post and like models:
# app/models/post.rb
class Post < ApplicationRecord
after_create :notify_pusher, on: :create
has_many :likes
def notify_pusher
Pusher.trigger('feed', 'new-post', self.as_json(include: :likes))
end
end
# app/models/like.rb
class Like < ApplicationRecord
after_save :notify_pusher, on: :create
belongs_to :post
def notify_pusher
Pusher.trigger('feed', 'new-like', self.post.as_json(include: :likes))
end
end
In the code above, we add an after_create
and after_save
callback to the post and like models respectively. These callbacks call the function to publish new posts and likes.
Updating the UI
Now that our server is publishing data each time it’s created, it’s up to the client to listen for those changes and do something with that data.
Lets rename our posts.coffee
file to posts.coffee.erb
and update it with the following code:
$(document).ready =>
currentUser = ''
welcomePage = $('.welcome-page')
postPage = $('.post-page')
newUserForm = $('#new-user-form')
<%# when user enters a username, store it and show the post page %>
newUserForm.on 'keyup', (event) ->
if event.keyCode == 13 and !event.shiftKey
currentUser = event.target.value
newUserForm.val('')
welcomePage.addClass('collapse')
postPage.removeClass('collapse')
greeting = """welcome @#{currentUser}"""
$('.current-user').html(greeting)
$('#username').val(currentUser)
return
<%# function for adding new posts to the feed %>
updateFeed = (post) ->
postTime = new Date(post.created_at.replace(' ', 'T')).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })
$('.posts').prepend """
<div class="post-wrapper col-12 mb-2 p-0">
<div class="card">
<div class="card-header">@#{post.username}<small class="float-right mt-1">at #{postTime}</small></div>
<div class="card-body">
<p class="card-text">#{post.post}</p>
<a class="far fa-thumbs-up add-like" data-remote="true" rel="nofollow" data-method="post" href="/likes/#{post.id}"></a>
<span class="ml-2" data-post="#{post.id}">#{post.likes[0].like_count}</span>
</div>
</div>
</div>
"""
return
<%# if the post was successfully saved, get the post and pass it to the updateFeed function %>
$('#post-form').on 'ajax:success', (data) ->
post = data.detail[0]
$('#post-form')[0].reset()
return
<%# suscribe our Pusher client to the feed channel. Whenever there is a new post or new like, update the view with it %>
pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
encrypted: true)
channel = pusher.subscribe('feed')
channel.bind 'new-post', (data) ->
updateFeed data
channel.bind 'new-like', (data) ->
<%# whenever there is a new like, find the liked post via it's "data-post" attribute then update its likes count %>
currentPost = $ 'span[data-post=\'' + data.id + '\']'
currentPost.text(data.likes[0].like_count)
return
return
In the code above, we subscribed our Pusher client to the feed
channel and listened for the new-post
and new-like
events. Once those events are emitted, we get the data and update the feed and likes count with it.
Bringing it all together
Restart the development server if it is currently running. Visit http://localhost:3000 in two separate browser tabs and test out the realtime feed and likes.
Conclusion
In this post, we have been able to create a realtime feed app using Pusher. I hope you found this tutorial helpful and would love to apply the knowledge gained here to easily set up your own application using Pusher.
You can find the source code for the demo app on GitHub.
23 May 2018
by Christian Nwamba