Using the SilverStripe CMS within Rails


RubyOnRails is a powerful framework, but it lacks a nice CMS frontend for the static parts of a website. In contrast to that in the PHP-world there are lots of ready to use CMS systems with such a feature. But the PHP systems are not as clean und elegant for implementing the dynamic parts of the website. There are also much more security issues with PHP-based CMS systems as there are in rails. The here discussed approach uses a popular PHP CMS (SilverStripe) for maintaining the static parts of the sites tree, while using RubyOnRails templating and runtime system (ActionPack) for actually running the site. With that setup you get both of two worlds: RubyOnRails secure and elegant web application framework and SilverStripes CMS frontend for comfortable editing the static pages of the sites tree.

Architecture

Both systems are installed separately, with separate virtual hosts and separate databases. They can run on the same server, with two virtual hosts in the webserver and separate databases on the database server and a separate document root. In this way a broken and compromized CMS opens only access to the static page data, but not to the dynamic web application (shop, blog, ...). The only thing that is shared between the two systems is one database table. In this table the CMS maintains the structure and content of the websites pages. The rails system has read-access to this table, but uses its own runtime system (ActionController and ActionView) to render the live pages and sending them to the browsers.

Database Access

The SilverStripe database user must grant select rights on the table which contains the CMS maintained sitetree to the RubyOnRails database user:

grant select on SiteTree_lang_Live to 'ober'@'localhost';

Within RubyOnRails this table is migrated (db/migrate/20090814113532_create_pages.rb) as database view which looks like a ActiveRecord standard table with:

class CreatePages < ActiveRecord::Migration
  def self.up
    execute('create view pages
  ( id, pagetype, url, title, menu, content, metatitle, metadesc, metakeywords, inmenu, sort, parent_id ) as
   select
   ID, ClassName, URLSegment, Title, ifnull(MenuTitle,Title), Content,
   MetaTitle, MetaDescription, MetaKeywords, ShowInMenus, Sort, ParentID
     from wirts.SiteTree_Live'
  )
  end

  def self.down
    execute 'drop view pages'
  end
end

and the following model makes the CMSs pages information accessible by ActiveRecord (app/models/page.rb):

class Page < ActiveRecord::Base
  acts_as_tree :order => "sort"
end

Routing

The RubyOnRails routing (config/routes.rb) takes care to route all urls which have no corresponding rails controller to the special "silverstripe" controllers method "emulate". This route is the last route when no rails controller matches the request:

ActionController::Routing::Routes.draw do |map|
  ...
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
  map.connect ':pagename/*path', :controller => 'silverstripe', :action => 'emulate'
end

Controller

The "silverstripe" controller (app/controllers/silverstripe_controller.rb) read the pages infos properties from the CMSs database. If not found a redirection to 404 is done which resembles the default routing behaviour of unmatched requests. If the page is valid the silverstripe view is rendered with the action set to the pagetype, which chooses the template.

class SilverstripeController < ApplicationController
  def emulate
    if @page = Page.find_by_url(params[:pagename])
       render :action => @page.pagetype
    else
       redirect_to :controller => '404.html'
    end
  end
end

Templates

SilverStripes default "Page" layout (themes/THEME/templates/Page.ss) is found in the RubyOnRails controller default layout (app/views/layouts/silverstripe.html.erb) and the the "PageType" layout is the action layout (app/views/silverstripe/Page.html.erb). Simply "translate" the layout files from SilverStripes templating engine syntax to Rails erb-syntax. The CSS-files of the template can be used unmodified from the SilverStripe theme and should be placed into Rails public CSS-area. The action is set by the controller by looking up the pagetype in the database. The templates must be copied from the SilverStripe system and all <% phpcode %> occurances have to be converted in corresponding ruby calls. These calls are implemented as helpers for the silverstripe view. For example the SilverStripes default blackcandy themes Page template (themes/blackcandy/templates/Layout/Page.ss):

<div class="typography">
        <% if Menu(2) %>
                <% include SideBar %>
                <div id="Content">
        <% end_if %>

        <% if Level(2) %>
                <% include BreadCrumbs %>
        <% end_if %>

                <h2>$Title</h2>

                $Content

        <% if Menu(2) %>
                </div>
        <% end_if %>
</div>

becomes a Rails erb template(app/views/silverstripe/Page.html.erb):

<div class="typography">
        <% if control_menu(2) %>
                <%= render :partial => "sidebar" %>
                <div id="Content">
        <% end %>

        <% if control_level(2) %>
                <%= render :partial => "bread" %>
        <% end %>

                <h2><%= @page.title %></h2>

                <%= @page.content %>

        <% if control_menu(2) %>
                </div>
        <% end %>
</div>

the blackcandy Includes (themes/blackcandy/templates/Includes/SideBar.ss and themes/blackcandy/templates/Includes/BreadCrumbs.ss) become partials in the Rails erb view (app/views/silverstripe/_sidebar.html.erb):

<div id="Sidebar" class="typography">
 <div class="sidebarBox">
    <h3>
        <% for p in control_level(1) %>
                <%=h p.title %>
        <% end %>
    </h3>

    <ul id="Menu2">
      <% for m in control_menu(2) %>
        <li class="<%= linkingmode(m) %>">
          <a href="<%= m.url %>" title="Go to the <%=h m.title %> page" class="<%= linkingmode(m) %>">
            <span><em><%= m.menu %></em></span>
          </a>
      <% end %>
    </ul>

    <div class="clear"></div>
 </div>
<div class="sidebarBottom"></div>
</div>

and (app/views/silverstripe/_bread.html.erb):

<% if control_level(2) %>
        <div id="Breadcrumbs">
                <p><%= breadcrumbs %></p>
        </div>
<% end %>

Helper

The silverstripe view helpers give the same functionality as the corresponding SilverStripe templating systems in (app/helpers/silverstripe_helper.rb):

module SilverstripeHelper

def base_tag
#<base href="http://$PROJ.wpack.de/" />
"<title>#{@page.metatitle}</title>"
end

def meta_tags
  <<EOF
<meta name="generator" http-equiv="generator" content="RoR SilverStripe Emulator - Powered by wirt: http://www.silverwirt.de" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="keywords" http-equiv="keywords" content="#{@page.metakeywords}" />
<meta name="description" http-equiv="description" content="#{@page.metadesc}" />
<meta http-equiv="Content-Language" content="en-US"/>
EOF
end

def control_menu (level)
  if level == 1
    Page.find(:all, :conditions => [ "parent_id = ? AND inmenu", 0 ], :order => "sort")
  else
    # traverse back to root and get children of the level
    p = @page
    s = [ p ]
    while p = p.parent
      s.unshift p
    end
    s[level-2].children.empty? ? false : s[level-2].children
  end
end

def control_level (level)
  # traverse back to root and get page of the level
  p = @page
  s = [ p ]
  while p = p.parent
    s.unshift p
  end
  s[level-1..level-1]
end

def control_page (url)
  [ Page.find_by_url(url) ]
end

def linkingmode(linkpage)
  linkpage == @page ? 'current' : 'link'
end

def breadcrumbs
  p = @page
  parts = p.title
  s = []
  while p = p.parent
    s.push p
  end
  s.push Page.find_by_url('home')
  s.each do |p|
    parts = "<a href=\"#{p.url}\">#{p.title}</a> &raquo; #{parts}"
  end
  parts
end

end

Assets

As last step a symbolic link to SilverStripes "assets" directory from RubyOnRails "public" directory is needed to make all images visible, and keeping all insite links intact.

Menü