A minimal CMS module for Ramaze apps...
August 1st, 2008
...and a valuable programming lesson
Preface
This post got longer than anticipated, because I refactored my code in the middle of writing this very post. The refactored code is at the bottom. Explanations are carelessly strewn about.
Main
I was going to write a post about how great Ramaze is and about how I have some ideas for coding conventions that could help with code reuse, sharing, and multi-app capabilities without requiring any new framework-specific code.
But not now - I need to write shorter posts to get some writing out the door at all. Therefore, I'm just posting some code that implements a simple (primitive?) content management system to keep any site copy in markdown (or textile) format.
module StaticPages
class MainController < Ramaze::Controller
def self.actions(*args)
args.each do |meth|
define_method(meth.to_sym) { render_template("#{meth}.txt") }
end
cache *args
end
map "/content"
view_root "view/content"
engine :Maruku
helper :cache, :aspect
actions :home, :help, :tos, :footer_snippet
# Optional expiry methods for convenience
def expire(page, pass = nil)
if pass == "secret"
path = "/content/#{page}"
action_cache.delete path
redirect path
end
end
# In dev mode put this in layout:
# A('Expire all caches', :href => "/content/expire_all/secret")
def expire_all(pass = nil)
if pass == "secret"
action_cache.clear
redirect "/"
end
end
end
end
Explanation
What you need with this is a view/content folder that contains the following files (for this example): home.txt, help.txt, tos.txt, footer_snippet.txt.
Then, when you write any markdown formatted text in those files it will be automatically rendered as html and action-cached. If you want to include these pages, just call render_partial("/content/home") in any template.
You can also expire the cached pages (see self-explanatory methods above). In dev mode you can just add a link in your footer to expire all caches and updating content becomes much less painless. No more hand-coding html for static pages in 2008!
Caveats
- The maruku (markdown) template engine hooks don't seem to be included with Ramaze 2008.06. You can get the source (just one file) from the ramaze repo (direct link) and require it in your app.
- If you want Textile, simply change
engine :Marukutoengine :RedCloth(note the capitalization). - You can't have any pages called "expire" or "expire_all"
Critical problem leading to redesign
While updating existing pages works in production via the expire actions, this system still requires a restart for newly created pages to show up, as you have to add them manually to the actions :page_name method. How to get around this?
- Parse the view/content directory for new files and add matching new methods on the fly.
- Do some method_missing tricks, hacking around the dispatcher, etc. (shudder)
- Do the simplest thing that works: use a parameterized action as a catch-all:
module StaticPages
class MainController < Ramaze::Controller
map "/content"
view_root "view/content"
engine :Maruku
helper :cache, :aspect
cache :show, :key => lambda{ request['page'] }
def show(page = nil)
page ? render_template("#{page}.txt") : redirect("/")
end
# Optional expiry methods for convenience
def expire(page, pass = nil)
if pass == "secret"
path = Rs(:show, page)
action_cache.delete path
redirect path
end
end
# In dev mode put this in layout:
# A('Expire all caches', :href => "/content/expire_all/secret")
def expire_all(pass = nil)
if pass == "secret"
action_cache.clear
redirect "/"
end
end
end
end
Improved design
The redesigned code has an almost identical interface to the original version (the path changed from /content/{page} to /content/show/{page}), but allows adding pages without restarting the server (or source reload). It's also slightly shorter, and clearer. You can now also name your pages expire or expire_all.
Finally, I'm reminded of one of my favorite programming aphorisms: "All problems can be solved by adding another layer of abstraction, except the problem of having too many layers of abstraction."
Call me unsophisticated, but I prefer simple solutions like this mini CMS. It's just a Ruby module containing a Ramaze controller that you simply copy to your project and call via require 'static_pages' in start.rb. No plugin architecture needed, you know your code, you know where it lives, and you can easily take it with you to another project. That's why I prefer Ramaze over Rails (or even Merb. Sinatra is simple too, but doesn't scale up to large projects, code-structure wise).
Calls to action
- How would you improve my code?
- It would be nice to start a repository of modules/slices of functionality for Ramaze apps to create an ecosystem/platform for code reuse (similar to rails/merb's plugins, but without the (unnecessary) plugin framework.
License
The source code in this article is released under an MIT license, copyright by ben@ the domain of this site. If you use this code, please consider telling me, as it would make me happy and motivate me.
August 1st, 2008 at 10:08 PM
You can structure your code with Sinatra any way Ruby will allow you too.