I am building a Rails 5 team management app which lets users manage organizations and users. I would like to be able to change from using the :id in the path (e.g: /organizations/43) and use an alpha-numeric slug instead (e.g: /organizations/H6Y47Nr7). Similar to how Trello do this (i.e: https://trello.com/b/M9X71pE6/board-name). Is there an easy way of doing this?
I have seen the FriendlyId gem which could take care of the slugging in the path but what would be the best way to generate the slug in the first place?
Ideally, for the most bang for buck the slug would include A-Z, a-z and 0-9 (as I understand it, this is Base58?) and for the sake of not blowing out the url too much, 8 characters at the most. If my calculations are correct, this gives 218 trillion combinations, which should be plenty.
Am I on the right track? Any help would be much appreciated.
Thanks
To create a slug, easiest way is to use SecureRandom. You can add something like the following in your model
before_create :generate_slug
private
def generate_slug
begin
self.slug = SecureRandom.urlsafe_base64(8)
end while Organization.exists?(slug: slug)
end
One small caveat here with respect to what you want is that the slug will sometimes contain an underscore or a dash but that should be fine.
irb(main):014:0> SecureRandom.urlsafe_base64(8)
=> "HlHHV_6rN3k"
irb(main):015:0> SecureRandom.urlsafe_base64(8)
=> "naRqT-NmYDU"
irb(main):016:0> SecureRandom.urlsafe_base64(8)
=> "9h04l4jEEsM"
If you go that route, I would create a table were you save the slugs that you are generating and don't delete them even when you delete an organization. When you create a new organization query this model to make sure there are no duplicates slugs. Also add a unique index in the slug column of the organizations table.
You should not give up the id column with integers so in the show method you will need to do:
org = Organization.where(slug: params[:id]).first
Related
On Rails 4.2, I would like to use Friendly Id for routing to a specific model, but dont wan't to create a slug column on the model table. I would instead prefer to use an accessor method on the model and dynamically generate the slug. Is this possible? I couldn't find this in the documentation.
You can not do this directly using friendly id because of the way it uses the slug to query the database (relevant source).
However it is not difficult to accomplish what you want. What you will need are two methods:
Model#slug method which will give you slug for a specific model
Model.find_by_slug method which will generate the relevant query for a specific slug.
Now in your controllers you can use Model.find_by_slug to get the relevant model from the path params. However implementing this method might be tricky especially if your Model#slug uses a non-reversible slugging implementation like Slugify because it simply get rids of unrecognized characters in text and normalizes multiple things to same character (eg. _ and - to -)
You can use URI::Escape.encode and URI::Escape.decode but you will end up with somewhat ugly slugs.
As discussed here I went with the following approach for custom routing based on dynamic slug.
I want custom route like this: /foos/the-title-parameterized-1 (where "1" is the id of the Foo object).
Foo model:
#...
attr_accessor :slug
#dynamically generate a slug, the Rails `parameterize`
#handles transforming of ugly url characters such as
#unicode or spaces:
def slug
"#{self.title.parameterize[0..200]}-#{self.id}"
end
def to_param
slug
end
#...
routes.rb:
get 'foos/:slug' => 'foos#show', :as => 'foo'
foos_controller.rb:
def show
#foo = Foo.find params[:slug].split("-").last.to_i
end
In my show view, I am able to use the default url helper method foo_path.
I am using slugs in my project to give my params an other name but I have two params called: "how-does-it-work".
(.../investor/how-does-it-work)
(.../customer/how-does-it-work)
I would like to use the slugs as how they are currently set.
Is there a way to do that?
Create two distinct routes/controllers, and simply query the corresponding ActiveRecord model in the show action. Assuming there is a slug field on your models:
Rails.application.routes.draw do
resources :customers
resources :investors
end
class CustomersController < ApplicationController
def show
#customer = Customer.find_by(slug: params[:id])
end
end
class InvestorsController < ApplicationController
def show
#investor= Investor.find_by(slug: params[:id])
end
end
This is probably the most conventional way to solve this problem in Rails. If you are using the friendly_id gem, the same approach more or less applies, except for maybe the query itself.
Hope this helps.
So, is /investor/ and /customer/ both parts of the slug?
If that's the case, you can split the string, and do a search based on the "how-does-it-work" in the grouping of "investor" or "customer".
If investor and customer are both parts of the routes, you shouldn't have a difficult time there, because they're pointing to two different controller methods. You should be able to write a search based on each of those methods that correspond to the data. If the data is the same, all your doing is pointing the controller to the correct model data with the correct params.
If you're using friendlyId, it usually has built in candidate matching. Also, if you're meaning to match multiple pages to the same slug (which I've done in the past), you can display a results page if you'd like too, by rendering based on the quantity of results.
I'm trying to use Friendly ID to create more custom urls. However, the column I want to use for a slug is still a number. I don't want to create a separate column just for the slug, since it would be identical.
I have a Lot that belongs to an Auction.
Auctions have many Lots (items for sale).
A Lot has a lot_number, which is unique to an Auction. It's not unique throughout the entire table, though. It's basically just a way to order the lots in each auction.
My URLs look like: /auctions/1/lots/21 (/auctions/:auction_id/lots/:id)
I want them to be: /auctions/1/lots/1 (/auctions/:auction_id/lots/:lot_number)
I added the following to lot.rb:
extend FriendlyId
friendly_id :lot_number
It almost works. It shows me a Lot with the correct lot number, but the wrong auction.
I read about scopes on the Friendly ID docs, which sounded perfect. I could scope the lot against the auction...
So I tried:
extend FriendlyId
friendly_id :lot_number, :use => :scoped, :scope => :auction_id
Now I see the following error:
SQLite3::SQLException: no such column: lots.slug: SELECT "lots".* FROM "lots" WHERE "lots"."slug" = '1' LIMIT 1
Why is the SQL WHERE lots.slug = 1? Shouldn't it be WHERE lots.auction_id = 1?
Am I using the wrong syntax? Not sure where I've gone wrong.
Any help would be appreciated. Thanks.
i think that you should have a look at from_param and to_param methods that are used in rails to lookup models: http://apidock.com/rails/ActiveRecord/Base/to_param
I would like to give John Doe the permalink john-doe-2, if there already is a john-doe-1.
The number should be the next free one to be appended ("john-doe-n")
Currently my permalinks are generated the usual way:
before_validation :generate_slug
private
def generate_slug
self.permalink = self.name.parameterize
end
How to implement a validates_uniqueness_of-like method, that adds this kind of number to self.permalink and then saves the user normally?
First of all, ask yourself: Is there a simpler way to do this? I believe there is. If you're already willing to add numbers to your slug, how about always adding a number, like the ID?
before_validation :generate_slug
private
def generate_slug
self.permalink = "#{self.id}-#{self.name.parameterize}"
end
This is a very robust way of doing it, and you can even pass the slug directly to the find method, which means that you don't really need to save the slug at all.
Otherwise, you can just check if the name + number already exists, and increment n by 1, then recheck, until you find a free number. Please note that this can take a while if there are a lot of records with the same name. This way is also subject to race conditions, if two slugs are being generated at the same time.
I just want to have default characteristic of ActiveRecord which uses incremental integers as id to reduce the length of the url.
For example, first article created will have url like "app.com/articles/1" which is default in ActiveRecord.
Is there any gem that supports this in mongoid?
You could always generate shorter, unique tokens to identify each of your records (as an alternative to slugging), since your goal is just to reduce the length of the URL.
I've recently (today) written a gem - mongoid_token which should take any of the hard work out of creating unique tokens for your mongoid documents. It won't generate them sequentially, but it should help you with your problem (i hope!).
You can try something like this:
class Article
include Mongoid::Document
identity :type => Integer
before_create :assign_id
def to_param
id.to_s
end
private
def assign_id
self.id = Sequence.generate_id(:article)
end
end
class Sequence
include Mongoid::Document
field :object
field :last_id, type => Integer
def self.generate_id(object)
#seq=where(:object => object).first || create(:object => object)
#seq.inc(:last_id,1)
end
end
I didn't try such approach exactly (using it with internal ids), but I'm pretty sure it should work. Look at my application here:
https://github.com/daekrist/Mongologue
I added "visible" id called pid to my post and comment models. Also I'm using text id for Tag model.
AFAIK it's not possible by design:
http://groups.google.com/group/mongoid/browse_thread/thread/b4edab1801ac75be
So the approach taken by the community is to use slugs:
https://github.com/crowdint/slugoid