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> » #{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.