Rails theme support with Metal

Metal is neat. Themes are neat. Together they are nifty. So, in the spirit of the upcoming St. Patrick’s day holiday I decided to play around with metal to add theme support to my rails app. I like how Wordpress supported themes by having meta data tucked into the stylesheet itself, and I also like query string (odd, I know) — so I figured I’d melt them together… in the spirit of the holiday and all.

The Theme Files

First things first. We need to create a themes folder. I chose RAILS_ROOT/public/stylesheets/themes but anywhere public would work. Inside there I threw the following stylesheet:

I made the theme support a minimum of 3 meta fields:

  1. theme — The name of the theme
  2. begin — What date to start using the theme
  3. end — What date to stop using the theme
  4. *enabled — Optional: If you want to enable this theme

You can hack on this and include as many as you want — they follow the same basic format as URLs:

theme.csstheme=My Themes Name&begin=March 17&end=March 18

/*
RAILS_ROOT/public/stylesheets/themes/st_patricks_day.css
{{
	theme=st patricks day
	&begin=March 17
	&end=March 18
	&enabled=1
}}
*/

#header{
	background:#C2DDCA url(theme_images/shamrock.png) no-repeat 25px 20px;
	padding-left:50px;
	}

The Metal

And, now into the neat part. Generate a rails metal file to handle theme switching on the fly, according to the current date.

$>./script/generate metal theme

Which should give you a nice app/metal/theme.rb file which you can play with.

I hacked together this code — and I’m not sure how stable it is, but should probably used in production with caution. It uses Rails.cache so make sure in your environment.rb file you have a cache store set.

class Theme
  # this is where the theme index is stored
  CacheKey = 'themes'
  # this is what we will look for in the layout
  ThemeEnvKey = 'rails.theme'
  # this is the path to your themes folder
  ThemesPath = File.join(RAILS_ROOT, 'public', 'stylesheets', 'themes')

  def self.call(env)
    # Build our theme index
    themes = Rails.cache.fetch(CacheKey) do
      data = {}
      Dir[File.join(ThemesPath, '*.css')].each do |theme|
        # looks for {{ url_type_string_here }} comment meta data and parses
        # it out into a hash for the theme's file name
        #
        # within your theme file, you would have:
        #   {{theme=my name&begin=DATE&end=DATE}}
        # becomes
        #   {'theme.css' => {:theme => 'my mane', :begin => 'START_DATE', :end => 'FINISH_DATE'}}
        #
        # DATE is parsed so it can be any type of
        # date-ish string see ActiveSupport::TimeZone#parse
        begin
          data[File.basename(theme)] = \
            $1.split('&').inject({}) do |h, v|
              s = v.split('=').map(&:strip)
              h[s.first.to_sym] = s.last
              h
            end if File.read(theme) =~ /\{\{(.+)\}\}/m
        rescue Exception => e
          data[theme] = {:error => e.to_s}
        end
      end
      data
    end

    # Set the appropriate theme
    now = Time.zone.now.beginning_of_day
    themes.each_pair do |file, data|
      next if data.keys.include?(:enabled) and data[:enabled].to_i.zero?
      next unless data.keys.include?(:begin) and data.keys.include?(:end)

      if Time.zone.parse(data[:begin]) <= now and Time.zone.parse(data[:end]) > now
        env[ThemeEnvKey] = file
        break
      end
    end

    # pass
    Rails::Rack::Metal::NotFoundResponse
  end
end

What this basically does is:

  1. Check within the ThemesPath for any .css files
  2. Reads them and parses out the metadata within the stylesheet
  3. Caches that
  4. Checks the date to see if any theme files should be turned on
  5. Sets an env key to let the rest of Rails know we want a theme

The Rails Layout

Well, now Metal has set our env['rails.theme'] key so we know a theme is in order it is time to put that theme to use.

Some DRYing is in order to make sure the metal and layout share the same env keys and paths, but this should get your basic themes goin.
# your applications main layout file (app/views/layouts/application.html.erb?)
# I put this after the normal stylesheet call, so our theme can override what we want instead of the entire stylesheet

<% if theme = request.env['rails.theme'] %>
  <%= stylesheet_link_tag "themes/#{theme}", :media => :all %>
<% end %>

Once all these steps are combined, and March 17th hits, we should now have a stylish header image that shows the shamrock.png! I can almost hear the users clinging glasses :)



Comments

No comments yet.

Add Yours

  • Author Avatar

    YOU


Comment Arrow



About Author

Rob Hurring

Ruby, Rails, PHP, bash... oh my!