Feb 04 2009

Use Rails to Create a Static Site

Category: Rails, dradisetd @ 2:00 pm

One of the new things we released last week with dradis v2.0 was a new web site for the project (dradis.sourceforge.net).

The old site consisted of 20 static pages or so, which was nice and easy but a real pain to maintain or restructure. So we thought that letting Rails do the heavy lifting for us would be a good idea, but we did not want to set up a Rail environment in the server…

What we finally did is use Rails as a tool to create a static site that we could .tar.gz and upload to the server. As a starting point we used a post in www.chuckvose.com and this is how we completed it to fit our needs.

Follow up (2009-03-23): do not miss how to integrate your rails-static site with Rake and Subversion in the second article of this series: Use Rails to Create a Static Site: Rake and Subversion.


The Controller

We just need one action in our main controller (PagesController):

class PagesController < ApplicationController
  layout 'main'
  def index
    @page = params.fetch(:id, 'index')
    expanded_page = "#{RAILS_ROOT}/app/views/pages/#{@page}.html.erb"
    exists = File.exists?( File.expand_path(expanded_page))
    if exists
      render :action => @page
    else
      render :text => "#{expanded_page} doesn't exist"
    end
  end
end

We fetch the :id parameter from the request that contains the name of the requested page (or index by default), then we try to locate the corresponding file (in /app/views/pages/) and if found we render the action. Rendering the action with no action code in the controller will just render it’s ERB template which is exactly what we want.

We also added a couple of routes in /config/routes.rb so every time a something.html was requested, the PagesController would be invoked:

map.root :controller => 'pages'
map.connect ':id.html', :controller => 'pages', :action => 'index'

The Layout and the Pages

The layout we used was quite simple. The most interesting bits follow.

First, we want each page to be able to set their own HTML title. This is accomplished by including the following code in the layout:-

<head>
  <title><%= yield :title %></title>
</head>

Then pages must include this line to set the contents of the :title variable:-

<% content_for :title do %>Announcements - dradis<% end %>

Other than that, we used the standard @content_for_layout in the main section of the page. However, the site has two menus, one in the top and one in the right hand side. This exposes a new challenge because we need to create the structure of the page in a way that is simple enough to maintain and add new content in the future. We had to code some Rails helpers to aid us in this.

The Helpers

The Top Menu

The Top menu bar consists only of the main sections of the site. In the layout we call the Helper straight away passing the current page (see The Controller above):

<div class="bar">
<ul>
  <%= barmenu(@page); %>
</ul>
</div>

The code of the helper in this instance is quite simple. The only intelligence we are adding is a check to see if the current page is one of the items in the menu, if it is, we add the CSS active class to the list item.

  def barmenu(page)
    items = ['download', 'demo', 'screenshots', 'documentation' ]
    items.collect do |i|
      active = ''
      label = i.capitalize
      if (page == i)
        active = ' class="active"'
      else
        label = "<a href=\"#{i}.html\">#{i.capitalize}</a>"
      end
      "<li#{active}>#{label}</li>"
    end
  end

Right Hand Side Navigation


The navigation menu in the right side was a bit trickier. We had our pages organised in three different sections: using dradis, developing dradis and support from.

Each section contains a number of items, some of the items have our content, some are links to external sites. In addition to this it would be useful if we could nest subsections as shown in the screenshot of the right.

So we had to figure out a way of representing the structure of the menu. Probably the neatest solution would have been to create an HTML page that contained a series of nested ul and li elements and then make the helper parse that structure assign the active CSS class to the right element and display it. Or maybe using a fancy JavaScript trick to locate the active item and assign the CSS class. However, as often happens we needed a solution, and it had to be fast, so we opted for having the structure loaded in a ruby array with one Hash per section:

$sections = [
  { ... }, # using dradis
  {
    :title => 'developing dradis',
    :pages => [
      {
        :title => 'info. for developers',
        :url => 'developers.html',
        :pages => [ :extensions ]
      },
      :roadmap,
      {
        :title => :bug_tracker,
        :url => 'http://sourceforge.net/tracker/?atid=1010917&group_id=209736&func=browse'
      },
      {
        :title => :feature_requests,
        :url => 'http://sourceforge.net/tracker/?atid=1010920&group_id=209736&func=browse'
      },
      {
        :title => :subversion,
        :url => 'http://sourceforge.net/svn/?group_id=209736'
      }
    ]
  },
  { ... } # support from
]

Each of the three section Hashes has a :title and a :pages attribute. The latter is an array containing the information of the pages associated with the parent section. The elements of the array can be either Symbols or new Hashes that describe the different pages.

We use a Symbol if the content of the page is provided by the site such as in the :roadmap case whose contents are in /app/views/pages/roadmap.html.erb and the link displayed in the menu would be “roadmap”. In more complex cases (i.e. we want a custom title for the link or a section that contains subsections) we use a Hash.

When a Hash is used at least a :title and a :url elements must be provided. Optionally a :pages element can be used to nest sections inside sections. The format of this :pages value is again an array as the one used in the top level section. So there you have your recursion!

The helper in this case is a bit more complex. First, the call in the layout, nothing fancy here:

<div>
  <%= rightmenu(@page) %>
</div>

The rightmenu() helper code:

def rightmenu(page)
  $sections.collect do |section|
    "<h3>#{section[:title]}:</h3>" + sectionmenu(:page => page, :section => section)
  end
end

Each main section is enclosed in h3 tags and then sectionmenu (a helper-to-the-helper method :roll: ) is invoked for each section. There are some tricks going on in the following piece of code, have a look and we will nail it down later:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
  def sectionmenu(options = {})
    current_page = options[:page]
    section = options[:section]
    level = options.fetch(:level, 0)
    css = options.fetch(:css, ['right_articles'])
    css |= ["submenu#{level}"] if (level > 0)
 
    section[:pages].collect do |page|
      # If the last item was the active one, clear the flag
      css.delete('active') if css.include?('active')
      title = ''
      url = ''
      complex = false
 
      # simple page (Symbol) vs. complex page (Hash)
      if (page.class == Hash)
        complex = true
        title = page[:title].to_s.gsub(/_/,' ')
        url = page[:url]
      else
        title = page.to_s.gsub(/_/,' ')
        url = page.to_s + '.html'
      end
 
      # active vs. inactive
      if (current_page + '.html' == url)
        css << 'active'
      else
        title = "<a href=\"#{url}\">#{title}</a>"
      end
 
      # subsections
      # tradeoff: calculate the sub section,
      # if it doesn't contain the "active" page, don't use it
      subsection = ''
      if (complex && page.key?(:pages))
        subsection =  sectionmenu(
            :page => current_page,
            :section => page,
            :level => level + 1
        )
 
        found = !(/active/ =~ subsection).nil?
        if ( !found && !css.include?('active') )
          subsection = ''
        end
      end
 
      "<div class=\"#{css.join(' ')}\">#{title}</div>" + subsection
    end.join
 
  end

In lines 2 to 6 we initialise the properties for this section. Then for each page in the section we apply the loop in 8 to 51. Due to the way the data is structured in the $sections variable, this code can be used to recursively crawl the tree of sections and subsections.

For each page in the section:

  • we initialise the internal variables (9 to 13). If the page is a Hash we extract the title and URL for the link from it’s elements, if it is a Symbol we infer the title and link from the symbol name.
  • Then we check if the current page in the iteration is the active one (26 to 30). If it is, we add a CSS attribute to the item, if it is not, then we add a hyper-link to it (it would not make sense to have a link to the page if you are already in it).
  • Finally in 36 to 47 we check if there is a :pages element in the Hash and if there is one, we make a recursive call to render the subsection.

Two things need to be said about the above piece of code. First, each subsection will be assigned a CSS class in accordance to it’s level: submenu1, submenu2, etc. This will allow us to style the different subsections appropriately. And second, because there is no easy way of knowing if the active belongs to one of the sub sections of the current section we make the recursive call, and then see if we have found the active page. If we have not, we discard the subsection altogether. We want this behaviour of not displaying a subsection in the menu unless the user is already in that section and this was the easiest way of implementing it.

The Bread Crumbs

The bread crumbs help our users to know where they are in the site (see picture in the right sid). We need to have a flexible helper that would display the path the user has followed to get into the page they are at the moment. Below is the code of the layout, just above the main content and with a check to verify that we are not in the main page:

<div class="left">
  <% if @page != 'index' %>
  <div id="breadcrums">
    <ul>
      <%= breadcrumsmenu(:page => @page) %>
    </ul>
  </div>
  <% end %>
  <%= @content_for_layout %>
</div>

And again, this helper will use the $sections as discussed above.

  def breadcrumsmenu(options={})
    crums = []
    $sections.each do |section|
      options[:section] = section
      crums = breadcrums(options)
      break if (crums.size >1)
    end
    this_page = crums.pop
    (crums.collect do |c| "<li><a href=\"#{c[:url]}\">#{c[:title]}</a>&lt/li&gt"  end).join + "<li>#{this_page[:title]}</li>"
  end

Again a similar tradeoff here, we cycle through all the sections trying to find the active page. We use the breadcrums helper-to-the-helper function to find out if the page is in the given section and then we render the bread crumbs. I realise that the two helper-to-the-helper functions are quite similar (cycle through a section trying to locate the active page), and possibly we could refactor both into a single function that does thing. But then again, once it worked, we did not have too much spare time to look into it, so we may improve it in the future, but this is the solution we came up with at that point. The code of breadcrums() is very similar in structure to the code in sectionmenu() but this time, it returns an array of pages that will lead us to the active one:

  def breadcrums(options={})
    current_page = options[:page]
    section = options[:section]
    crums = options.fetch( :crums, [ {:title => 'home', :url => '/'} ])
 
    section[:pages].each do |page|
      title = ''
      url = ''
      complex = false
      found = false
 
      if (page.class == Hash)
        complex = true
        title = page[:title].to_s.gsub(/_/,' ')
        url = page[:url]
      else
        title = page.to_s.gsub(/_/,' ')
        url = page.to_s + '.html'
      end
 
      if (current_page + '.html' == url)
        found = true
        crums << { :title => title, :url => url }
      end
 
      if (!found && complex && page.key?(:pages))
        subsectioncrums = breadcrums(
          :page => current_page,
          :section => page,
          :crums => [{:title => title, :url => url}]
        )
        if (subsectioncrums.size > 1)
          crums += subsectioncrums
        end
      end
 
      break if (crums.size > 1)
    end
    return crums
  end

Finishing Touch

Finally, to generate the static pages that we could compress and upload to the server, some good old wget magic was used:

<br />
wget -m -nH http://localhost:3000/<br />

Popularity: 100% [?]

Share and Enjoy:
  • Digg
  • del.icio.us
  • Slashdot
  • Technorati

Tags:

9 Responses to “Use Rails to Create a Static Site”

  1. Alno says:

    Did you try tools like staticmatic or webby? It think, it may be some simpler, than using rails for this task.

  2. etd says:

    I didn’t try them because at the time they seemed to be a bit overkilling. However, I have to be honest and when I first thought about it I didn’t realise that the recursive section thing was going to be so *interesting*. Do you know if it is possible to have this kind of behaviour with webby?

  3. Alno says:

    Hmmm.. I don’t know any built-in support for recursive sections in webby or staticmatic. It’s simply generates set of pages based on templates and some helpers.

    Possible, some third-party helpers implementing such behavior exist.

  4. jojo siao says:

    Hi,

    Thanks for the tutorial. I can learn more from this tutorial.

  5. etd says:

    Your welcome!

  6. usefulfor.com/ruby » Use Rails to Create a Static Site: rake and subversion says:

    [...] have already seen how to Use Rails to Create a Static Site. In that article we left the site running, and we recommended the use of wget to generate the [...]

  7. just2click says:

    Is there any source code which includes all these cool things as a sample?
    I tried to get the latest version of dradis and I can’t find most of the things.
    As a new developer with Ror (but not a new developer at all) I need such a sample very much.

    Thanks in advance

  8. etd says:

    This code is used to handle the website of dradis (http://dradisframework.org/), it is not in dradis itself :) However I see that a small example would be useful, I will try to work on it as soon as I get the time!

  9. just2click says:

    Thanks a bunch.
    I’m really eager to start RoRing

Leave a Reply