Recently I've been working on a multi-tenant blog hosting application with a Rails front end, and this required us to let users set their own domains. Here's how to dynamically route your Rails application with multiple domains and subdomains (of yours and theirs).
In my application there is the concept of a Site
which always has a slug
, which becomes slug.mydomain.com
. And sometimes has a domain
, which belongs to them.
What we need to do is match the request based on a constraint
which is simply a class we create, which implements the matches?
method. This is very simple as you will see below:
routes.rb
...
class CustomDomainConstraint
# Implement the .matches? method and pass in the request object
def self.matches? request
matching_site?(request)
end
def self.matching_site? request
# handle the case of the user's domain being either www. or a root domain with one query
if request.subdomain == 'www'
req = request.host[4..-1]
else
req = request.host
end
# first test if there exists a Site with a domain which matches the request,
# if not, check the subdomain. If none are found, the the 'match' will not match anything
Site.where(:domain => req).any? || Site.where(:slug => request.subdomain).any?
end
end
match '/', :to => 'sites#index', :constraints => CustomDomainConstraint, via: :all
match '/category/:slug' => 'sites#category', :constraints => CustomDomainConstraint, via: :all
match '/:slug', to: 'sites#show', :constraints => CustomDomainConstraint, via: :all
...
It's worth noting here that if your app allows users to specify their own domains, you should have a system of blacklisting your own domain/s and the www
subdomain/slug.
Then all I need to do in the sites_controller.rb
is implement a before_filter
which finds the Site
based on some request data, this is the code I used:
before_filter :find_site
...
private
def find_site
# generalise away the potential www. or root variants of the domain name
if request.subdomain == 'www'
req = request.host[4..-1]
else
req = request.host
end
# first test if there exists a Site with the requested domain,
# then check if it's a subdomain of the application's main domain
@site = Site.find_by(domain: req) || Site.find_by(slug: request.subdomain)
# if a matching site wasn't found, redirect the user to the www.<root url>
redirect_to root_url(subdomain: 'www') unless @site
end
All you need to do, or have your users do is CNAME
their domain name to your main domain which points to your app.
My application lives on Heroku, and they use virtual hosts to route domain names, this means we need to tell them when we want to CNAME
a new domain to our instance. To do this I just implemented an after_save
on my Site
model which uses Heroku's API to register a new domain with my application.
site.rb
after_save do |site|
heroku_environments = %w(production staging)
if site.domain && (heroku_environments.include? Rails.env)
added = false
heroku = Heroku::API.new(api_key: ENV['HEROKU_API_KEY'])
heroku.get_domains(ENV['APP_NAME']).data[:body].each do |domain|
added = true if domain['domain'] == site.domain
end
unless added
heroku.post_domain(ENV['APP_NAME'], site.domain)
heroku.post_domain(ENV['APP_NAME'], "www.#{site.domain}")
end
end
end
I was looking around the internet for a while before I figured out how to do this, so I hope this makes your day easier :) Comment below or tweet me if you have any questions or suggestions!