Jake
Jake

Reputation: 1186

How to add the name of a blog post's category to route url with Rails 7

I have been trying to add the name of the BlogCategory that a BlogPost belongs to in a URL such as this:

sitename.com/blog/category-name/blog-post-title

At the very least, I want this to render for the show of the BlogPost but am okay with it being the url for every action such as new, edit, and destroy.

I'm using the friendly_id gem, if that makes a difference.

BlogCategory Model:

class BlogCategory < ApplicationRecord
  extend FriendlyId
  friendly_id :name, use: :slugged
  has_many :blog_posts

  # This is a self referential relation. This is where records in a table may point to other records in the same table.
  has_many :sub_categories, class_name: "BlogCategory", foreign_key: :parent_id
  has_many :sub_category_blog_posts, through: :sub_categories, source: :blog_posts
  belongs_to :parent, class_name: 'BlogCategory', foreign_key: :parent_id, optional: true
  # This is a scope to load the top level categories and eager-load their posts, subcategories, and the subcategories' posts too.
  scope :top_level, -> { where(parent_id: nil).includes :blog_posts, sub_categories: :blog_posts }

  def should_generate_new_friendly_id?
    slug.nil? || name_changed?
  end
  
end

BlogCategory Controller:

class BlogCategoriesController < ApplicationController
  before_action :admin_user, only: [:new, :create, :edit, :update, :destroy]
  before_action :set_blog_link, only: [:show, :edit, :update, :destroy]

  ...

  private

  def cat_params
    params.require(:blog_category).permit(:name, :parent_id, :sub_category, :summary)
  end

  def main_cat
    @cat = BlogCategory.parent_id.nil?
  end

  def set_blog_link
    @blog_link = BlogCategory.friendly.find(params[:id])
    redirect_to action: action_name, id: @blog_link.friendly_id, status: 301 unless @blog_link.friendly_id == params[:id]
  end

end

BlogPost Model:

class BlogPost < ApplicationRecord
  extend FriendlyId
  friendly_id :title, use: :history
  belongs_to :blog_category
  validates  :title, presence: true, length: { minimum: 5 }
  validates  :summary, uniqueness: true
  default_scope {order(created_at: :desc)}

  def should_generate_new_friendly_id?
    slug.nil? || title_changed?
  end
end

BlogPost Controller:

class BlogPostsController < ApplicationController
  before_action :admin_user, only: [:new, :create, :edit, :update, :destroy]
  before_action :set_post_link, only: [:show, :edit, :update, :destroy]
  before_action :find_post, only: :show

  ...

  private

  def post_params
    params.require(:blog_post).permit(:title, :body, :summary, :thumbnail_link, :blog_category_id)
  end

  def find_post
    @post = BlogPost.friendly.find(params[:id])

    # If an old id or a numeric id was used to find the record, then
    # the request path will not match the post_path, and we should do
    # a 301 redirect that uses the current friendly id.
    if request.path != blog_post_path(@post)
      return redirect_to @post, :status => :moved_permanently
    end
  end

  def admin_user
    redirect_to(root_url) unless current_user.admin?
  end

  def set_post_link
    @post_link = BlogPost.friendly.find(params[:id])
    redirect_to action: action_name, id: @post_link.friendly_id, status: 301 unless @post_link.friendly_id == params[:id]
    end

end

Here is relevant code from my routes.rb file:

Rails.application.routes.draw do

  resources :blog_categories, path: 'blog'
  resources :blog_posts

end

What I've tried

I've tried the following without success but have little understanding of what I'm doing:

get 'blog/:blog_category_name/:blog_post_title', to: 'blog_posts#show', as: 'blog_post'

and also tried

resources :blog_posts, path: 'blog/:blog_category_name/:blog_post_title', except: [:new, :create]
resources :blog_posts, only: [:new, :create]

With this in my BlogPost controller inside the show method/block:

@post_url = BlogPost.find_by(title: params[:blog_post_title], blog_category_id: params[:blog_category_name])

I even tried adding the params used in the routes to the permitted list under post_params.

I also tried making a new post to see if old posts weren't linking properly because of the url structure change.

The URL's I'm getting are not utilizing the parameters I'm passing to them.

Upvotes: 0

Views: 189

Answers (1)

max
max

Reputation: 102423

What you're doing here is really just a nested resource but with a vanity route and and slugging which doesn't actually require such a heavy hand.

The typical controller for a nested resource would look like this:

class BlogPostsController < ApplicationController
  before_action :set_blog_category
  before_action :set_blog, only: [:show, :edit, :update, :delete]

  # GET /blog/foo/bar  - your custom vanity route
  # the conventional route would be 
  # GET /blog_categories/foo/blog_posts/bar
  def show
  end

  # GET  /blog/foo/blogs_posts     -> index
  # GET  /blog/foo/blogs_posts/new -> new
  # POST /blog/foo/blogs_posts    -> create

  # ...  

  private

  def set_blog_category
    @blog_category = BlogCategory.friendly.find(params[:blog_category_id])
  end

  def set_blog
    @blog = Blog.friendly.find(params[:id])
  end
end

Besides the fact that you're using friendly.find you don't actually need to do anything to do the lookup via slugs instead of the id column. If you want to find the records only by their friendly id (and not allow numerical ids) use the find_by_friendly_id method instead.

Note that :id (or _id) in a parameter name is not equal to the id column - it's just a name for the unique indentifier segment in the URI pattern.

While you can configure the name of the param its actually kind of silly as in Rails things just work when you stick with the conventions.

You can just define the vanity route for this as:

resources :blog_categories, path: 'blog', only: [] do
  # the typical routes nested under "blog_posts"
  resources :blogs_posts, only: [:new, :create] 
  # your custom vanity route should be defined last to avoid conflicts
  resources :blogs_posts, path: '/', only: :show
end

Generating the URL can be done either by calling the named blog_category_blog_path helper or by using the polymorphic route helpers:

blog_category_blog_path(@blog_category, @blog_post) 
redirect_to [@blog_category, @blog_post]
form_with model: [@blog_category, @blog_post]

If you have legacy URLs using a different structure that you want to redirect I would consider using a separate controller or just doing the redirect in the routes to separate out the responsibilities from this controller.

You also should avoid duplicating the authorization/authentication logic across your controllers (your admin_user method). Thats how you get security holes.

Upvotes: 1

Related Questions