![]() |
Write a Widget in 15 Minutes with Ruby on RailsMonday June 2nd 2008 |
In this article, I’ll show you how easy it is to get a widget up and running with Ruby on Rails. For the purpose of this article, a familiarity with Rails 2.0 or higher is assumed. If you would like to follow along with the actual code, feel free to download it first.
The first thing we need is an idea for our widget. For the sake of simplicity, let’s say we wanted to display a list of the most popular searches on Yahoo!. If you head on over to the Yahoo! Buzz Index, you’ll see that we can pull from several RSS feeds to get the information we’re after. For this widget, we’ll be displaying the top searches in the following categories: overall, actors, movies, music, sports, tv and video games.
Creating the Rails Application
The first thing we need to do is generate our application code and databases:
$ rails ybuzz $ rake db:create:all
Generating the Model
Next, we will generate a Category model to represent each of our 7 search categories. Each category will have a name and a feed url:
$ ./script/generate model Category name:string feed_url:string
Now we need to create a new model for each category. Since this is a fairly static list of categories, there is no need to create an interface for category creation. Instead, we can just set up each of our categories in the migration file, in the self.up method:
overall = Category.new( :name => "Overall", :feed_url => "http://buzz.yahoo.com/feeds/buzzoverl.xml") actors = Category.new( :name => "Actors", :feed_url => "http://buzz.yahoo.com/feeds/buzzactl.xml") movies = Category.new( :name => "Movies", :feed_url => "http://buzz.yahoo.com/feeds/buzzmovl.xml") music = Category.new( :name => "Music", :feed_url => "http://buzz.yahoo.com/feeds/buzzmusl.xml") sports = Category.new( :name => "Sports", :feed_url => "http://buzz.yahoo.com/feeds/buzzsportl.xml") tv = Category.new( :name => "Television", :feed_url => "http://buzz.yahoo.com/feeds/buzztvl.xml") videogames = Category.new( :name => "Videogames", :feed_url => "http://buzz.yahoo.com/feeds/buzzgamesl.xml") overall.save actors.save movies.save music.save sports.save tv.save videogames.save
Now we need create the database table for categories and populate it:
$ rake db:migrate
In order to get RESTful routes for categories, we can add the following line to routes.rb, which will give us the method, category_path, that we can use in our views to link to specific category pages.
map.resources :categories
The Category model is where the meat of our application code will go. We want to be able to take a particular category, and have it give us all of the top searches from the Yahoo! Buzz Index in that category. To do this, we will create an instance method, named searches, in our Category class, which fetches an XML feed, and returns a list of top searches in its category. To make things interesting, let’s also see if we can find a photo on Flickr which corresponds to the search term, for a little more visual appeal. In order to use the Flickr API, you will need to sign up for an API key. We can create a variable in our environments.rb file to store our key, so that it will be available to us throughout the application.
FLICKR_KEY = "insert_flickr_key_here"
Now that we have a key for the Flickr API, we can start fetching and parsing the XML for our Buzz feed. To do this, we will be using the Hpricot gem. If you haven’t used this before, I recommend reading through the documentation on the website. It’s a very easy to use XML parser, which allows us to use XPath to target nodes in a document. We will also be using open-uri to fetch the actual XML document, so you may need to require that library in your Category model. Opening an XML document with Hpricot and open-uri is very easy. First, let’s take a glance at what our feed actually looks like:
<?xml version="1.0" encoding="iso-8859-1" ?>
<rss version="0.91">
<channel>
<title>Yahoo! Buzz Index Leaders Overall</title>
<link>http://buzz.yahoo.com/overall/</link>
<description>Yahoo!'s most popular search terms.</description>
<language>en-us</language>
<copyright>Copyright 2008 Yahoo! Inc. All rights reserved</copyright>
<managingEditor>buzz-help@yahoo-inc.com</managingEditor>
<item>
<title>1. American Idol</title>
<link>http://search.yahoo.com/search?p=american+idol&cs=bz</link>
</item>
<item>
<title>2. Jessica Alba</title>
<link>http://search.yahoo.com/search?p=jessica+alba&cs=bz</link>
</item>
...
</channel>
</rss>
And the code to fetch that feed and turn it into something we can use:
doc = Hpricot.XML(open("http://buzz.yahoo.com/feeds/buzzoverl.xml"))
This will return an Hpricot::Doc object which we can traverse to find the nodes we are interested in. Since this is an RSS feed, the nodes we care about are all the item elements in the document. The expression to return all these elements is:
items = doc.search("//item")
Once we have all the elements in our feed, we can iterate over them and attempt to find a photo from Flickr that relates to the search term.
items.each do |item|
title = item.at("title").innerHTML.gsub(/[0-9]*.s?/,'')
photos_url = "http://api.flickr.com/services/rest/?method=flickr.photos.search
&api_key=#{FLICKR_KEY}&text=#{CGI::escape(query)}
&per_page=5&license=1,2,3,4,5,6"
photos_doc = Hpricot.XML(open(photos_url))
photos = photos_doc.search("//photo")
if !photos.empty?
photo = photos[rand(photos.length)]
user_id = photo["owner"]
user_url = "http://api.flickr.com/services/rest/?method=flickr.people.getInfo
&api_key=#{FLICKR_KEY}&user_id=#{user_id}"
user_doc = Hpricot.XML(open(user_url))
user_name = user_doc.at("person").at("username").inner_html
image = {
:url => "http://farm#{photo["farm"]}.static.flickr.com/#{photo["server"]}/
#{photo["id"]}_#{photo["secret"]}.jpg",
:owner => user_name
}
else
image = false
end
searches << { :title => title, :image => image }
end
This code first finds the title element within the item element and does a regular expression replacement of the leading numeral (1., 2., etc.) returned by the Yahoo! Buzz feed. Next, it constructs a Flickr API photos.search url using our Flickr API key and a url-escaped version of the search query from the feed. This url returns 5 Creative Commons licensed photos whose title or description match the search query. It then uses Hpricot to fetch an XML document from Flickr and parses out all 5 of the photo elements from it. Since we don’t want to see the same photo every time, we select a random element from this array of photos. The last step is to find the username of the owner of each photo so that we can attribute the source of the photo in our widget. This requires making a call to the people.getInfo API method.
Some minor refactoring of our model code, and this is our final category.rb:
require 'hpricot'
require 'open-uri'
class Category < ActiveRecord::Base
def searches
doc = Hpricot.XML(open(self.feed_url))
return parse_searches(doc)
end
protected
def parse_searches(doc)
searches = Array.new
items = doc.search("//item")
items.each do |item|
title = item.at("title").innerHTML.gsub(/[0-9]*.s?/,'')
image = get_flickr_image(title)
searches << { :title => title, :image => image }
end
searches
end
def get_flickr_image(query)
begin
timeout(2) do
url = "http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=#{FLICKR_KEY}
&text=#{CGI::escape(query)}&per_page=5&license=1,2,3,4,5,6"
doc = Hpricot.XML(open(url))
photos = doc.search("//photo")
return false if photos.empty?
photo = photos[rand(photos.length)]
user_id = photo["owner"]
user_url = "http://api.flickr.com/services/rest/?method=flickr.people.getInfo
&api_key=#{FLICKR_KEY}&user_id=#{user_id}"
user_doc = Hpricot.XML(open(user_url))
user_name = user_doc.at("person").at("username").inner_html
image = {
:url => "http://farm#{photo["farm"]}.static.flickr.com/#{photo["server"]}/
#{photo["id"]}_#{photo["secret"]}.jpg",
:owner => user_name
}
image
end
rescue Timeout::Error
return false
end
end
end
Generating The Controllers and Views
The next step is to create our controllers. We will need two controllers - one for the main “splash” page which gives the user the option of choosing which category they want to view and one for displaying the top searches for each category.
$ ./script/generate controller buzz $ ./script/generate controller categories
At this point, it is worth mentioning that a Yahoo! widget must send a Content-Type: application/x-ywidget+xml header. It is also strongly recommended to send a Cache-Control: no-cache header in order to avoid your widget’s dynamic content from being cached, particularly for the Java Go client. So, let’s create a before_filter in our application controller which sets the necessary headers for each response:
class ApplicationController < ActionController::Base
before_filter :set_content_type
protected
def set_content_type
response.headers["Content-Type"] = "application/x-ywidget+xml"
response.headers["Cache-Control"] = "no-cache"
end
end
In order for us to have a blueprint template we can use for our views, we need to create a new layout in app/views/layouts named application.html.erb:
<page>
<content>
<header layout="simple">
<layout-items>
<block class="title">Most Popular Y! Searches</block>
</layout-items>
</header>
<%= yield %>
</content>
</page>
This is a blueprint layout with one simple header element. The layout yields to our action views, where the main content for each page will go.
Now we can create the view for our main controller, which just lists the available categories. It will only have one index method. Here, we will find all categories and store them in an instance variable so that they will be available to the view.
def index @categories = Category.find(:all) end
The view for this action is where we will create the blueprint to show a list of all the categories. So, create a file named index.html.erb in app/views/buzz:
<module>
<header layout="simple">
<layout-items>
<block class="title">Select a Category</block>
</layout-items>
</header>
<% @categories.each do |category| -%>
<placard layout="simple" class="link">
<layout-items>
<block class="title"><%= category.name -%></block>
</layout-items>
<load event="activate"
resource="<%= category_path(category).sub('/','') -%>" />
</placard>
<% end -%>
</module>

Rendered Blueprint for Splash Page
We create one module element where our list of categories goes. Inside of that, we have a simple header element and a series of placard elements, one for each category. Each placard lists the category’s name (Overall, Actors, Movies, etc.) and links to its corresponding page. It’s worth noting here that we are using our RESTful routes for categories with the category_path method. However, we need to remove the first slash from the path, i.e. /categories/1, to turn it into categories/1. If we don’t do that, the widget platform will create the link //categories/1.
Now we can create the actions and views for displaying each category’s top searches in the categories controller. Since we are using RESTful routes, the categories/1 path will map to the show method of the categories controller. This will be the only action we create in this controller.
def show # get category from params array @category = Category.find(params[:id]) # get the top searches from buzz feed @searches = @category.searches # load all categories for navigation @categories = Category.find(:all) end
This code finds the category with the given id extracted from the GET parameters and loads its top searches. We also need to load all the categories so that we can display links to those category pages in our view. So now we need to create a view file called show.html.erb in app/views/categories:
<options>
<% @categories.each do |category| -%>
<option>
<label><%= category.name -%></label>
<load event="activate"
resource="<%= category_path(category).sub('/','') -%>" />
</option>
<% end -%>
</options>
<module>
<header layout="simple">
<layout-items>
<block class="title"><%= @category.name -%></block>
</layout-items>
</header>
<% @searches.each do |search| -%>
<placard layout="card" class="link">
<layout-items>
<% if search[:image] -%>
<image resource="<%= search[:image][:url] -%>" />
<% end %>
<block class="title"><%= search[:title] -%></block>
<% if search[:image] -%>
<block class="subtext">*photo credit: <%= search[:image][:owner] -%></block>
<% end -%>
</layout-items>
<load event="activate"
resource="http://beta.m.yahoo.com/m/search?q=<%= CGI::escape(search[:title]) -%>" />
</placard>
<% end -%>
<block class="small gray">*All photo credits are cited as Flickr usernames.</block>
</module>

Rendered Blueprint for Sports Cateogory Page
First, we create an options element to link to all of our category pages. Next, we open a module element, and display a simple header with the name of the category being shown. Finally, we iterate over the category’s top searches, displaying a placard element for each one. The placard contains the Flickr photo and attribution, if available, and the search query. Each placard links to a oneSearch for that particular search query.
And that’s it! The last thing left to do is package our application for deployment.
Packaging Your Widget
As with all widgets you wish to deploy, you must create a directory structure like the following:

Widget Packaging File Structure
The important file here is config.xml. Here’s an example:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://mobile.yahoo.com/widgets/schema/1.0/config/"
xmlns="http://mobile.yahoo.com/widgets/schema/1.0/config/">
<title>Yahoo! Search Buzz</title>
<version>0.1</version>
<identifier>ysearchbuzz</identifier>
<description>See the most popular searches on Yahoo!</description>
<icon>buzz</icon>
<author organization="Yahoo!" href="http://michael.vc"
email="mj@yahoo-inc.com">Michael Johnston</author>
<widget base="http://michael.vc:21000">
<preview>
<icon>buzz</icon>
<label>Y! Search Buzz</label>
</preview>
<shortcuts>
<item default="true">
<label>Main</label>
<href>buzz/index</href>
</item>
</shortcuts>
</widget>
</config>
In this case, we are serving our rails application on port 21000, although it is up to you to serve your widget however you like. And don’t forget that the email attribute in the author element is now required for all widgets.
Besides the config.xml, you also need to create a gallery.xml which is used when users are browsing your widget in the Widget Gallery. Lastly, you’ll need to create icons for your widget. For an in-depth overview of how to make excellent icons that look well across all devices, take a look at this article.
The final step is zipping up this directory and submitting it for testing. Once you test it out in your Yahoo! account, and make sure everything works as expected, you can submit it for real to the Widget Gallery, and watch as users subscribe to your widget.
So, that’s a quick overview of how you can take a widget from concept to completion in 15 minutes with Ruby on Rails. There are certainly other considerations to keep in mind when creating a full-featured widget you intend to deploy to users, such as internationalization. But I hope this gives you Rails developers out there a jump start into the Mobile Widget Platform. So get out there and start developing!
You can pick up the source code here.
