We use Radiant CMS as the framework for the Cultural District website, so it made sense to use the same framework when the time came to rebuild the Trust’s. One requirement we have for the Trust’s site that we did not for the District was the use of Vanity URLs: you know, how if you want to make it easy for someone to get to some really long url, you can make up a nice short url with a memorable word that directs people on to the right place. saalonmuyo.com/awesome redirects to, say…well, let’s face it. Anywhere that goes is rickrolling someone. You get the point.
So, how did we do it?
Basically, whenever you go to a URL in Radiant, it falls through a few steps. First, it checks if there are any defined routes you’ve set up in your *_app_extension.rb file. Failing that, it falls into Radiant’s SiteController, which searches every single Page to see if the path the user entered matches any of the Pages’ slugs. If it finds something there, it processes and loads that page. If it fails, Page’s find_by_path method returns the FileNotFoundPage you have defined. (This is assuming you have a FileNotFoundPage defined, which you should. Here’s how.) The SiteController then processes the FileNotFound page and displays that.
In order to define a Vanity URL, we needed to get in the middle of that process. We created a VanityUrlPage class – Radiant allows you to create custom page types – with two custom fields: Vanity URL and Target URL. Then, we overrode Page’s find_by_path method so that, if it failed to find anything, before we returned that FileNotFoundPage, we tried to see if a VanityUrlPage existed that matched. Then, we interrupted the SiteController so that, before it processed the page, it checked to see if it had found a VanityUrlPage and, if it did, redirected the user appropriately. So, how about some code?
This is all assuming you have a Radiant extension you’re developing. If not, you could create an extension just to do this. But let’s assume you already have an extension created. Within vendor/extensions/<your_extension>/ directory is a lib directory. Within that, I created an extensions directory, and within that, I created two files: page_extension.rb and site_controller_extensions.rb.
Within page_extensions.rb, we needed to override find_by_path. (Note that my module is called SatelliteAppExtensions. Yours will be called, YourNameExtensions instead.)
module SatelliteAppExtensions
module PageExtensions
def self.included(base)
base.class_eval do
alias_method_chain :find_by_path, :vanity_urls
end
end
def find_by_path_with_vanity_urls(path, live = true)
raise MissingRootPageError unless root
page = root.find_by_path_without_vanity_urls(path, live)
if page.is_a?(FileNotFoundPage)
vanity_url = VanityUrlPage.find_vanity_url_by_path(path, live)
end
vanity_url ? vanity_url : page
end
end
end
So, what’s that doing? Well, alias_method_chain is - as Brennen would call it – some hot sticky ruby magic that lets you say that when someone calls find_by_page, they actually call find_by_page_with_vanity_url. From now on, the only way to get directly to find_by_page is to call find_by_page_without_vanity_url. We use this so that we can implement some of our own logic around the find_by_page call. Essentially, we immediately call find_by_page_without_vanity_url, and if we get that FileNotFoundPage back, we run a query on the VanityUrlPages to see if we get a match. That’s the find_vanity_url_by_path call. Let’s look at that.
VanityPage
class VanityUrlPage < Page
def clean_target_url
self.target_url.match('http://') ? self.target_url :
VanityUrlPage.clean_path(self.target_url)
end
class << self
def find_vanity_url_by_path(path, live = true)
vanity_pages = VanityUrlPage.find(:all,
:conditions => "vanity_url like '%#{path}%'")
vanity_pages.each do |vanity_page|
return vanity_page if clean_path(path) ==
clean_path(vanity_page.vanity_url)
end
nil
end
def clean_path(path)
"/#{ path.to_s.strip }/".gsub(%r{//+}, '/')
end
end
end
In here, we do a search against all VanityUrlPages’ vanity_url field. If we get a match. Since we’re doing a SQL like query here, we might get a few false positives, so we then iterate through the results and see if any match exactly. The worry being that you have a /broadway Vanity URL and a /broadway/plays one and might get both back. If we get nothing back, we return nil. Oh, and clean_url just lets us whack any preceding or trailing slashes off the urls so they’re formatted the same.
Now, we’ve done what we need to find the Vanity URL page. But Radiant’s just going to assume it’s a normal page and render it, which we don’t want. We need it to redirect. Thus enters our site_controller_extensions.rb file.
module SatelliteAppExtensions
module SiteControllerExtensions
def self.included(base)
base.class_eval do
alias_method_chain :process_page, :vanity_page
end
end
def process_page_with_vanity_page(page)
if page.is_a?(VanityUrlPage)
redirect_to page.clean_target_url
else
process_page_without_vanity_page(page)
end
end
end
end
When Radiant finds a page, the SiteController runs it through a process_page call that essentially renders the thing for the user. We need to not do that for a VanityUrlPage. So, if the returned page is a VanityUrlPage, we redirect_to the target_url for that page. (And, actually, we redirect to a clean_target_url, the method for which is found above, that normalizes what /’s are there for relative URLs). If not, we pass the page along to the SiteController’s normal process_page method for normal handling.
Finally, we need to make sure Radiant knows to load your extensions to its core functionality. There is a file named <your_extension>_extension.rb in your extension’s root directory. For instance, for mine, it’s satellite_app_extension.rb. Open that up, and you’ll see a method named “activate”. Within that, you can define all kinds of things. In this case, we’re defining three things. Two we’ve talked about (requiring those extension files), and one we haven’t (adding custom VanityURL Fields on the administration side). Give me a minute and we’ll get to the other thing. For now, just do something like this:
require 'lib/extensions/page_extensions'
require 'lib/extensions/site_controller_extensions'
admin.page.edit.add(:form, "vanity_url_fields",
:after => 'edit_page_parts')
SiteController.send :include,
SatelliteAppExtensions::SiteControllerExtensions
Page.send :include, SatelliteAppExtensions::PageExtensions
What’s going on here? First, we’re requiring the code files for our extensions. Second, we’re telling the SiteController and Page to include the modification we’ve made to them.
Now, about that vanity url stuff in the middle. One thing I’m not going over in detail is creating the Vanity URL page itself. The tutorial I linked is pretty comprehensive and I don’t want to rewrite a bunch of information that might go out of date anyway. But what you need to do to make this work comes in two parts. You need to create the custom page fields for vanity_url and target_url, and you need to make those fields available on the VanityUrl administration page. I’m going to copy you my migration file to add the fields and the partial code needed to show those fields. Use those in the appropriate steps in that tutorial. And ask if you have any questions.
Migration:
class CreateVanityUrlPages < ActiveRecord::Migration
def self.up
add_column :pages, :vanity_url, :string
add_column :pages, :target_url, :string
end
def self.down
remove_column :pages, :vanity_url
remove_column :pages, :target_url
end
end
Partial (_vanity_url_fields.html.haml):
- if @page.is_a?(VanityUrlPage)
%br
%hr
.vanity_url
= label :page, :vanity_url, "Vanity URL"
= text_field :page, :vanity_url, :size => 100
%br
.target_url
= label :page, :target_url, "Target URL"
= text_field :page, :target_url, :size => 100
%br
%br
And that’s it. Have fun with your redirections! If you have any questions, comments or think this approach is bunk, let me know in the comments.