Rails and Subdomains
About a week ago, I quietly relaunched this blog to run off the same app as my main website. I wanted the transition to be as seamless as possible which meant:
- Keeping the same url:
blog.edwardloveall.com
- RSS readers wouldn’t get bombarded with “new” posts
This post will mostly focus on the “same url” part, but I’ll quickly describe how I made the RSS part work.
Preserving RSS posts
RSS items can have a guid
. From the RSS spec:
guid stands for globally unique identifier. It’s a string that uniquely identifies the item. When present, an aggregator may choose to use this string to determine if an item is new.
My old blog was hosted on tumblr. It uses the post’s permalink for guids and they look like this:
http://blog.edwardloveall.com/post/148454710842
This is fine for tumblr, but from now on, I’m going to use a reverse DNS system for guids:
com.edwardloveall.blog.rails-and-subdomains
To make this work, each post has an optional field called tumblr_guid
. This takes the form of the post id. For example, the post above would have a tumblr_guid
of 148454710842
.
I wanted to keep existing post’s guids the same, so if a post has an existing tumblr_guid, I use that. Otherwise I use a new, reverse-DNS style guid.
If you want to dive into the technical details, here’s the commit with the code.
On to subdomains!
This is going to be a quick tour the steps required to get subdomains working for this blog. It turned out to be pretty simple, after I spent enough time poking around. If you’re looking to do this for your app, just follow along.
The routes
I needed to add routes to manage incoming requests for the blog
subdomain. The Rails guides list a subdomain option on the constraint method which sounded exactly right. Here’s an example:
constraints(subdomain: 'blog') do
get '/', to: 'posts#index', as: :posts
end
I thought this would work without any other modifications, and it does in theory. But as I found out, it’s not so easy to try out.
The web server
I booted up the app with puma (the default Rails web server) and visited blog.localhost:3000
to test my new routing constraint. Unfortunately depending on the browser, either my subdomain was ignored and Rails rendered the main site or the browser gave me an error.
This is because localhost
isn’t a real domain. Real domains use DNS which allow subdomains like blog.edwardloveall.com
to work. The best way around this problem is using a service like lvh.me. It’s a domain that redirects requests to localhost
. Since it’s a real domain it uses DNS, and subdomains work like you’d expect. If I boot up my server now, I can visit blog.lvh.me:3000
and see my test blog page.
But the site still didn’t load. This is because of what addresses the default Rails server listens to. Starting in version 4.2, it only listens to 127.0.0.1
(i.e. localhost
), rather than 0.0.0.0
like it used to. 0.0.0.0
is a reserved IP address that listens to all addresses. So when I boot up my server, I need to bind it to 0.0.0.0
like so:
$ rails s -b 0.0.0.0
Now I could visit blog.lvh.me:3000
!
If you are on wifi in a public place, this is a security risk because anyone on that network with your IP address can access your app. Only use it when you need to.
Curiously, a brand new app with Rails 5 doesn’t seem to need the address binding. I’m not sure what configuration is different but your milage may vary.
The path helpers
So now I had an app responding to subdomains, but I still needed to test it and link to it from my main website.
The solution was to use *_url(subdomain: :blog)
helpers everywhere instead of *_path
. Path helpers return a relative path which implies the same domain name and completely ignore the subdomain
option.
We can see this in the console:
irb> app.posts_path(subdomain: :blog)
=> "/"
irb> app.posts_url(subdomain: :blog)
=> "http://blog.example.com/"
Keep this in mind when:
- Linking to your subdomain from site navigation
- Visiting your subdomain in tests
- Accessing RSS/Atom feeds
Here are some code examples:
post = Post.first
redirect_to post_url(post, subdomain: :blog)
<%= link_to 'Blog', root_url(subdomain: :blog) %>
link_to root_url(subdomain: :blog)
expect(current_url).to eq(post_url(post, subdomain: :blog))
And I just want to underscore that *_path
helpers with subdomain: :blog
does nothing. I can’t tell you how many times I tried to use this.
DNS configuration
The DNS setup was pretty straightforward. I removed my old CNAME record for blog
and added it again as an A record pointing toward the same address as the main site.
I’ll use this as an opportunity to say that if domains and DNS confuse the hell out of you, I wrote a book about them. Go check it out if you feel out of your depth with domain names and DNS configurations.
So that’s pretty much it. The rest was going through the motions of adding pretty urls, tests, code highlighting, an RSS feed, and pagination. Like I said, adding the subdomain, once you know the tricks, is pretty simple. Now my blog and portfolio can exist side-by-side in a single app.
Thanks for reading!