Search code examples
ruby-on-railspermalinksunique-constraintfriendly-idwildcard-subdomain

URL Permalinks for pages AND users (similar to facebook) for Rails4. Friendly_id?


Perhaps I'm misinterpreting the capabilities of the Friendly_id gem, but I haven't found any other way to accomplish this goal either:

I have an app where users get their own URL to their page, similar to what facebook does (e.g. http://www.notfacebook.com/mypermalink). I already have this capability working. Also similar to facebook, I'd like to enable pages to have their own URL (e.g. http://www.notfacebook.com/pagepermalink).

I added the friendly_id gem, thinking that it had the capability to check for uniqueness among User.permalink and Page.permalink - uniqueness across columns that are in two different tables/models. Instead, I get a pages URL pattern that looks like http://www.notfacebook.com/pages/pagepermalink.

I can't use

resources :pages, path: ''

nor

get '/:friendly_id', to: 'pages#show'

in routes.rb because that doesn't work with my existing Users permalink routes.

Is there a way to get unique page and user permalinks in my app?

I was considering custom subdomains instead of permalinks, but I use heroku and from what I've read, even though I have my own domain name, I cannot use subdomains on heroku. Is that correct? (Yeah, that's a separate question.)


Solution

  • There is a nice solution to that problem using routes constraints.

    Using routes constraints

    As the rails routing guide suggests, you could define routes constraints in a way that they check if a path belongs to a user or a page.

    # config/routes.rb
    # ...
    get ':permalink', to: 'users#show', constraints: lambda { |request| User.where(permalink: request[:permalink]).any? }
    get ':permalink', to: 'pages#show', constraints: lambda { |request| Page.where(permalink: request[:permalink]).any? }
    

    The order defines the priority. In the above example, if a language and a category have the same name, the language wins as its route is defined above the category route.

    The above solution requres the models to have a permalink column that defines a url-friendly name. But if you already have an url-friendly attribute, e.g. User#alias, you can use this as well:

    # config/routes.rb
    # ...
    get ':alias', to: 'users#show', constraints: lambda { |request| User.where(alias: request[:alias]).any? }
    get ':permalink', to: 'pages#show', constraints: lambda { |request| Page.where(permalink: request[:permalink]).any? }
    

    Using a Permalink model

    If you want to make sure, all paths are uniqe, an easy way would be to define a Permalink model and using a validation there.

    Generate the database table: rails generate model Permalink path:string reference_type:string reference_id:integer && rails db:migrate

    And define the validation in the model:

    class Permalink < ApplicationRecord
      belongs_to :reference, polymorphic: true
      validates :path, presence: true, uniqueness: true
    
    end
    

    And associate it with the other object types:

    class Page < ApplicationRecord
      has_many :permalinks, as: :reference, dependent: :destroy
    
    end
    

    This also allows you to define several permalink paths for a record.

    page_about_rails.permalinks.create path: 'rails'
    page_about_rails.permalinks.create path: 'ruby-on-rails'
    

    With this solution, the routes file has to look like this:

    # config/routes.rb
    # ...
    get ':permalink', to: 'users#show', constraints: lambda { |request| Permalink.where(reference_type: 'User', path: request[:permalink]).any? }
    get ':permalink', to: 'pages#show', constraints: lambda { |request| Permalink.where(reference_type: 'Page', path: request[:permalink]).any? }
    

    And, as a side note for other users using the cancan gem and load_and_authorize_resource in the controller: You have to load the record by permalink before calling load_and_authorize_resource:

    class Page < ApplicationRecord
      before_action :find_resource_by_permalink, only: :show
      load_and_authorize_resource
    
      private
    
      def find_resource_by_permalink
        @page ||= Permalink.find_by(path: params[:permalink]).try(:reference)
      end
    end