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:
- 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:
This is fine for tumblr, but from now on, I’m going to use a reverse DNS system for guids:
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
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.
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
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
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
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.
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!