I'm using the Friendly_Id 5 gem in my Rails 4 app to make the URL's "friendlier." I can currently generate URLs like /article/my-article-title. On many blog's today, you also see the date or the id in the path. How would I generate URLs like:
/articles/23/my-article-title
where 23 is the article id. It seems like the slug would actually have to be 23/my-article-title
since slugs have to be unique in the DB. Or how would I generate a date based slug like?
/articles/2014/01/22/my-article-title
I wouldn't put date of the article in the URL, as some people are thrown off by content that isn't created, like, within a week from now, so that might result in a little less traffic for you.
However, I would suggest to put ID next to the title. I'm not sure how to do this with friendly_id, but in my opinion there's a better way. I myself switched from friendly_id slugs to this method.
Simply define to_param
in your model:
def to_param
"#{id}-#{title.parameterize}"
end
Rails will be automatically able to find the id
using Model.find(params[:id])
I ended up doing the following.
In my routes.rb file:
resources :articles, except: [:index, :show]
constraints section: /section|anothersection|somethingelse/ do
constraints year: /\d{4}/ do
constraints month: /\d{1,2}/ do
resources :articles, only: :show, path: '/:section/:year/:month', as: :my_articles
get ':section/:year/:month', to: 'articles#by_month', as: :month
end
get ':section/:year', to: 'articles#by_year', as: :year
end
get ':section', to: 'articles#by_section', as: :section
end
Someone suggested I do this differently, but I ended up just using my version anyway since this allows me to add constraints directly to the routes. It's also visually clearer to see what my intentions are with this routes nesting.
In my article.rb model file I have something like:
class Article < ActiveRecord::Base
def slugify
to_remove = %w(a an as at before but by for from is in into like of off on onto per since than the this that to up via with)
title.downcase.split.reject {|word| to_remove.include?(word)}.collect {|word| word.gsub(/[:,\-\.’']/,'')}.join(' ').parameterize
end
def to_param
slug
end
end
I made the slugify method, which behaves like Drupal's default slug generator. This is what gets saved to the database inside the slug column. The to_param method then calls this field to generate the article slug.
As for my database schema.rb file, the important part literally boils down to creating the slug column and indexing it:
create_table "articles", force: true do |t|
t.string "title"
t.text "body"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "user_id"
t.string "section"
t.string "slug"
end
add_index "articles", ["slug"], name: "index_articles_on_slug", unique: true
And my controller looks like:
class ArticlesController < ApplicationController
before_action :set_section, only: [:by_month, :by_year, :by_section]
#I'm using the by_star gem to make the by_year and by_month part easy!
def by_month
@articles = Article.by_month(params[:month]).by_year(params[:year]).by_section(@section)
end
def by_year
@articles = Article.by_year(params[:year]).by_section(@section)
end
def by_section
@articles = Article.by_section(@section)
end
private
def set_section
@section = params[:section]
end
end
The result
Doing as I've explained avoids having to see unnecessary numbers in the slug. It also makes it possible to access articles by section, by year, by month and, obviously, show the article itself. You may want to handle everything via a single route as explained in the link, but that's up to you.
Hope this helps!