Reputation: 1186
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
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
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