Rails: "Next post" and "Previous post" links in my show view, how to?

I used the model methods as below to avoid the issue where id + 1 doesn't exist but id + 2 does.

def previous
  Post.where(["id < ?", id]).last
end

def next
  Post.where(["id > ?", id]).first
end

In my view code, I just do this:

<% if @post.previous %>
  <%= link_to "< Previous", @post.previous %>

<% if @post.next %>
  <%= link_to "Next >", @post.next %>

If each title is unique and you need alphabetical, try this in your Post model.

def previous_post
  self.class.first(:conditions => ["title < ?", title], :order => "title desc")
end

def next_post
  self.class.first(:conditions => ["title > ?", title], :order => "title asc")
end

You can then link to those in the view.

<%= link_to("Previous Post", @post.previous_post) if @post.previous_post %>
<%= link_to("Next Post", @post.next_post) if @post.next_post %>

Untested, but it should get you close. You can change title to any unique attribute (created_at, id, etc.) if you need a different sort order.


This is how I did it. Firstly, add a couple of named scopes to your Post model:

def previous
  Post.find_by_id(id - 1, :select => 'title, slug etc...')
end

def next
  Post.find_by_id(id + 1, :select => 'title, slug etc...')
end

Note the use of the :select option to limit the fields because you probably don't want to retrieve a fully-populated Post instance just for showing the links.

Then in my posts_helper I have this method:

def sidebar_navigation_links
  next_post = @post.next
  previous_post = @post.previous
  links = ''
  if previous_post
    links << content_tag(:h3, 'Previous')
    links << content_tag(:ul, content_tag(:li,
                              content_tag(:a, previous_post.title,
                                          :href => previous_post.permalink)))
  end
  if next_post
    links << content_tag(:h3, 'Next', :class => 'next') if previous_post
    links << content_tag(:h3, 'Next') if previous_post.nil?
    links << content_tag(:ul, content_tag(:li,
                              content_tag(:a, next_post.title,
                                          :href => next_post.permalink)))
  end
  content_tag(:div, links)
end

I'm sure this could be refactored to be less verbose, but the intent is clear. Obviously your markup requirements will be different to mine, so you may not choose to use an unordered list, for example.

The important thing is the use of the if statements because if you're on the first post then they'll be no previous post and conversely, if you're on the last post they'll be no next post.

Finally, simply call the helper method from your view:

<%= sidebar_navigation_links %>

My method will allow you to automatically use model scopes. For example, you may only want to display posts that are "published."

In your model:

def self.next(post)
  where('id < ?', post.id).last
end

def self.previous(post)
  where('id > ?', post.id).first
end

In your view

<%= link_to 'Previous', @posts.previous(@post) %>
<%= link_to 'Next', @posts.next(@post) %>

In your controller

@photos = Photo.published.order('created_at')

Associated RSpec tests:

describe '.next' do
  it 'returns the next post in collection' do
    fourth_post = create(:post)
    third_post = create(:post)
    second_post = create(:post)
    first_post = create(:post)

    expect(Post.next(second_post)).to eq third_post
  end

  it 'returns the next post in a scoped collection' do
    third_post = create(:post)
    decoy_post = create(:post, :published)
    second_post = create(:post)
    first_post = create(:post)

    expect(Post.unpublished.next(second_post)).to eq third_post
  end
end

describe '.previous' do
  it 'returns the previous post in collection' do
    fourth_post = create(:post)
    third_post = create(:post)
    second_post = create(:post)
    first_post = create(:post)

    expect(Post.previous(third_post)).to eq second_post
  end

  it 'returns the previous post in a scoped collection' do
    third_post = create(:post)
    second_post = create(:post)
    decoy_post = create(:post, :published)
    first_post = create(:post)

    expect(Post.unpublished.previous(second_post)).to eq first_post
  end
end

Note: there will be small issues when you reach the first/last post in a collection. I recommend a view helper to conditionally show the previous or next button only if it exists.