Vanity URLs in Radiant CMS

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.

This entry was posted in Coding. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *