Upload
sean-cribbs
View
6.390
Download
0
Embed Size (px)
DESCRIPTION
Citation preview
Content Management That Won’t Rot
Your BrainSean Cribbs
complicated
http://flickr.com/photos/koolgary/2460635163/Content management on the web can be pretty complicated.
hundreds
http://flickr.com/photos/carowallis1/485124848/There are hundreds of open-source content management systems to choose from,
in-completion
http://flickr.com/photos/yakobusan/2436481628/all in various states of completion.
un-intuitive
http://flickr.com/photos/mashed_potatoe/65823367/Most of them have unintuitive user interfaces,
tangled
http://flickr.com/photos/randomurl/440190706/tangled content models,
ugly code
http://flickr.com/photos/flickerbulb/187044366/
and opaque, poorly-architected and undocumented code that refuses to yield to customization attempts.
content model
These include abominations like ezPublish, which is so in love with its content model
content model
that it seems they implemented the editing interface in the content model.
huh?
http://flickr.com/photos/fdecomite/402499198/Good luck figuring that one out.
pluggable
http://flickr.com/photos/studiosmith/2085278030/Or Xaraya, that despite its ultra-pluggable code modules with proscribed interfaces
bridge to nowhere
http://flickr.com/photos/garlandcannon/3048731338/doesn't seem to do anything it advertises.
template surgery
http://flickr.com/photos/soldiersmediacenter/1148977208/Or Joomla & Mambo, which require template surgery to create a custom look and feel
everything to everyone
http://flickr.com/photos/twose/887903401/while trying to be everything to everyone.
FTP is so 1971
http://flickr.com/photos/daveseven/167903362/Why is it so hard to publish a web page that I don't have to use FTP to edit?
webstandards
Why can't I use HTML, CSS and the other web standards I know and love?
ideal CMS
So what would I want in my ideal CMS?
#1 : simple
First, it would be simple to use and understand.
meta is overrated
I'm not impressed with hyper-abstract meta-content models, just make it work.
#2 : standards markup
Second, it would let me write markup and presentation in any format and structure I like,
design freedom
giving me the freedom to design my site the way I want.
#3 : powerful tools
Third, it would have simple and powerful tools built-in to make most content-generation tasks a breeze,
PHP not required
without needing plugin modules.
#4 :clear code
Fourth, it would have a well-architected code design
easy customization
so I could understand and customize it easily.
drum roll please
Fortunately, there's a CMS that lets me do all these things.
It’s Radiant!
It's called Radiant, and it's written in our favorite language, Ruby.
DHHAPPROVED
And yes, even David likes it.
#1 :simple content
model
So how does Radiant meet my expectations? First, Radiant's content model is easy to understand.
Pages
Every Radiant site is made up primarily of Pages,
acts_as_tree
which are arranged in a tree, much like a directory/file structure.
has_many :parts
Pages have many content pieces called parts,
logical pieces
which let you break your pages into logical sections
content_for
think "content_for" blocks in Rails views.
Page-Types(STI)
Any page can have a special type, essentially a subclass of the Page model,
page-level plugins
that acts as a localized plugin, modifying the front-end behavior of that page.
Layouts
Pages also have layouts,
design details
which let you abstract common design details, just like Rails layouts,
Content-Type: text/html
and also let you specify the content type - be it HTML
Content-Type: text/xml
XML
Content-Type: text/css
CSS, or whatever.
Snippets
To round out the system are Snippets,
render :partial
which are kind of like Rails partials
repetitive
small pieces of common or repetitive content
contextual
that can be rendered in the context of many pages.
Filters
Pages and snippets can use text filters
TextileMarkdown
etc.
that let you write content in shortcut languages like Textile or Markdown.
template language
This is all tied together with a template language
Radius
called Radius
non-evaluating
that keeps code well-separated from your content.
#2 : complete design
control
Second, Radiant lets you design your site the way you want.
any text
It doesn't care whether you're publishing HTML, CSS, Javascript, plain text or whatever.
(no binary)
You probably don't want to put binary data in a page, but any kind of text is OK.
tags expose functionality
Most built-in and plugin functionality is exposed through template tags, so you don't have to worry that some feature will produce bad markup;
write your own markup
you can write the markup yourself.
#3 : powerful tools
Third, Radiant has powerful tools
over 50 tags
about 50 built-in template tags to help you accomplish many content-generation tasks.
navigation, sucker
You can use tags to generate navigation
row-striping
stripe table rows
feed me, Seymor
collate content into a feed
no logo for you!
http://flickr.com/photos/rafastarix/2729553586/
or display the big company logo only on the homepage.
#4 : clean code
Last but not least, our reason for being here
well-architected
Radiant is well-architected
developer goodies
and has plenty of goodies for developers.
show me the code
Since this is a technical talk, I'm going to gloss over the details of using Radiant to create a site and focus on how we can customize Radiant to our own needs.
Extensions
To this end, Radiant has a souped-up plugin system called "extensions"
plugins + awesome
which picks up where Rails plugins leave off.
merb-slices
Extensions are more like `merb-slices`
Rails Engines
or Rails Engines than plugins.
app/controllersapp/helpersapp/modelsapp/views
An extension can have its own "app" folder with controllers, helpers, models and views.
db/migrate
An extension can define database migrations
public
public files
vendor/plugins
and even its own plugins.
TATFT
Most importantly, you can "test all the time" in your extensions
RSpec
because Radiant provides a full RSpec harness
dataset :pages
a bunch of test datasets
@page.should render(“foo”)
and some nifty matchers to help out with Radiant-specific stuff.
class FooExtension < Radiant::Extension
In addition to the directory and file structure, every extension is expressed as a singleton class
def activate
that can provide some startup code in the "activate" method
define_routes do |map| map.resources :fuzzy_bearsend
route definitions
description “My first extension.”url “http://foo.com”
and some metadata that is displayed in the user-interface.
TIMTOWTDI
Depending on the scenario, there are a number of different ways we can customize Radiant to accomplish our goals, all of which can be nicely packaged up in extensions.
I could easily fill many hours talking about all the ways to tweak Radiant with extensions, so I'm going to give you the 10,000-foot view of the primary techniques.
#1 :new tags
The first and simplest way to add functionality is to define new Radius tags. As I said before, Radius is the template language used to expose dynamic functionality to the designer and content editor,
<r:awesome />
through the use of these 'r'-prefixed XML-like tags.
(not) XML
The tags seem to imply an XML-structure, but I assure you they are strictly not XML and are really just interpolated in-place.
<r:
Radius tags are composed of four parts - the 'r' prefix;
<r:children:each
the tag name, which may be colon separated;
<r:children:each order=“desc”>
the attributes;
<r:children:each order=“desc”> foo</r:children:each>
any nested contents, and a closing tag. A few notes on these tag names: Radius will execute the definition of each of those colon separated names in order and determine which definitions to use based on context.
<r:children><r:each order=“desc”> foo</r:each></r:children>
The colon-separated version is just a shortcut for nesting them, so this snippet is logically equivalent to the last one we saw.
<r:children><r:each order=“desc”> foo</r:each></r:children>
Also, the last named tag in the colon-separated list gets the attributes passed to its definition.
<r:stylesheet url=“/mine.css” />
So let's make a tag that generates some content - say a stylesheet link, and we'll make the tag look like this.
module LazyTags
First, we'll make a module in our extension to hold the tag definition.
include Radiant::Taggable
Include "Radiant::Taggable" and we're ready to start defining tags.
tag ‘stylesheet’ do |tag|
Next we'll start our tag definition with the `tag` keyword, the tag name, and a block yielding one parameter.
url = tag.attr[‘url’]
Inside our tag definition block, we'll grab the 'url' attribute off the tag,
%Q[<link rel=”Stylesheet” type=”text/css” href=”#{url}” />]
and interpolate that into the proper place in the HTML `link` tag.
end
The return value of the tag-definition block is what is rendered, so we're done.
def activate Page.send :include, LazyTagsend
Last, we mix the module into the Page model in the 'activate' method of our extension, and we have access to our new tag definition.
|tag|
The yielded 'tag' object inside a definition is very powerful.
TagBinding === tag
It's your access to the contextual parsing environment, including what page is being rendered, the request and response objects, and any variables set by surrounding tags.
tag.globalseverywhere
Its primary properties are 'globals', which gives you access to the global environment, including the current page being rendered;
tag.localscascading, contextual
‘locals' which is a cascading symbol-table of sorts, contextual to the current parsing environment;
tag.attrHash
'attr', a hash of attributes placed on the current tag;
tag.double?<r:foo>text</r:foo>
'double?', which is true when the tag contains other tags or text,
tag.single?<r:bar />
and 'single?' which is true when the tag is self-closing.
be kind, I’m sensitive
Because many tags are sensitive to their surrounding environment, you can easily do things like iteration, changing some locals property on each step of the iteration, and causing contained tags to use that property.
children.map do |child| tag.locals.page = child tag.expandend.join
This is essentially what the `r:children:each` tag does - iterate on each of the children, changing `tag.locals.page` on each iteration, then joining the output. To trigger rendering of a tag's contents, we just call `tag.expand`.
tag.render(“title”)
You can also directly invoke the rendering of another tag using `tag.render`. The result will be returned to you as a string.
tag.block
One neat trick is capture the contents of a container tag and reuse it in multiple places with `tag.block`.
<r:navigation urls=”Foo:/foo |Bar: /bar”>...</r:navigation>
The built-in `r:navigation` tag uses this to render navigation links depending on whether the current page matches the URL being output.
tag.locals.hash = {}tag.expand# ...
First, it sets up a hash that can be used inside nested tags, then renders its contents.
tag ‘navigation:here’ do |tag| tag.locals.hash[:here] = tag.blockend
The contained "navigation:here" tag assigns its block into the created hash.
# for each link...if url == page.url tag.locals.hash[:here].callend
Then the "navigation" tag calls the "here" block when the passed URL exactly matches the current page's URL. It uses the same technique to render prefix-matching and non-matching links by capturing blocks.
#2:Admin UI
So now that we can customize page output with tag definitions, let's look at the next big area of customization -- the admin UI.
class Widget < ActiveRecord::Base
Let's say you've built a model for widgets
<r:widgets />
and some Radius tags to display them in a page,
CRUD
and now you need to create an interface for CRUDing them.
map.resources :widgets
The first thing you might want to do, after creating your controller,
Add “Widgets” Tab
is to add a tab to the interface so your widgets are navigable.
Extension.admin
Radiant exposes an "admin" object to every extension that lets you tweak the interface
admin.tabs.add “Widgets”, “/admin/widgets”
and easily add tabs. As well as letting you simply add tabs,
:before => “Layouts”
you can also specify where they occur in the tab order
:visibility => [:admin]
and who can see them. But that's just a simple case of how to customize the UI.
a dash of spice
Often what you want to do is just add a little piece to one view or another.
widget list
Say you need to be able to see your widget list while editing a page.
fine-grained regions
Luckily, you can inject a view partial into any number of defined "regions" in the view template you want to modify, without overwriting the whole thing. This is enabled again through the admin object.
app/views/admin/pages/_widget_list.erb
To add our widget list, we just put it in the app/views/admin/pages directory,
admin.pages.edit.add :form_top, ‘widget_list’
and add it to the proper region like so. In this case, we’re adding it to the form_top region of the edit view for the pages controller.
include_stylesheet “widgets”include_javascript “widgets”
Inside your partial, in addition to simply rendering, you can add stylesheets and javascript files to the head block in the layout so you can make your widget list look awesome.
brute-force override
If you don't like the finessed approach, you can brute-force override any view template,
application.html.haml
including the default layout,
extensionsbefore core
by putting one of the same name in your extension, since extension view paths are searched before Radiant's built-in paths.
#3:front-end
So now you've got your admin interface doing cool things, but you want to provide a little more functionality on the front-end.
page rendering
Let's take a look at the process of how a page is rendered.
map.connect “*url”
First, any request URL that doesn't match an admin or extension controller gets sent to the splatted route
SiteController#show_page
assigned to SiteController.
request.get? orrequest.head?# read cache
If the request method is GET or HEAD, the controller checks whether the requested URL is cached, populating the response if it is.
show_uncached_page(url)
Otherwise, it tries to show an uncached version of the page.
@page = find_page(url)
The controller tries to find the page by the given URL,
process_page(@page)
and then processes it if found
@page.process(request, response)
calling page.process with the request and response
@cache.cache_response(url, response)
caching the response where appropriate.
render :template => “site/not_found”,:status => 404
If it's not found, you get the default 404 page,
redirect_to welcome_path
and if there's no pages at all in the database, you get redirected to the login screen.
SiteController#find_page
So within this process are a number of extension points to play with - `find_page`,
SiteController#process_page
`process_page`,
Page#process(req, resp)
and `Page#process`.
conversion SEO
Let's say that we have an old site that has some existing URLs that we want to preserve for a bit for SEO purposes.
choose a Page class
Since when we create a new page in the interface, Radiant lets us choose the page class we want,
redirect oldies
let's create a new Page class that performs a redirect from the old URL to our new location.
class RedirectPage < Page
So we'll choose to override the `process` method in our new subclass that we'll call `RedirectPage`.
page.part(:body).content == “/old/url”
We'll assume that we put the new URL in the 'body' part of the page,
301 Moved Permanently
and we'll want to consider this a permanent redirection.
def process(req, res) url = parse_object part(:body)
To accomplish this, inside we'll just render the body part to get our URL,
res.redirect url, “301 Moved Permanently”end
then cause the response to redirect.
def cache?; true; end
Believe it or not, a redirect can be cached!
5-minute cachewith headers
Radiant nicely captures the response headers and status when caching pages so, if we want to cache this, it won't have to call our process method again for 5 minutes after the first render.
GET HEAD cache
Now you may have noticed that when I walked through the SiteController workflow, it only checks for cached versions of a page on GET or HEAD requests.
POST PUT DELETEdon’t cache
This means that when a page receives POST, PUT, or DELETE, you can do cool stuff with the data in the request.
mailerdatabase_form
For example, the mailer and database_form extensions respectively process email forms and store data to the database on POST requests,
seamless experience
allowing a seamless front-end experience.
#4:I want my MVC
This is great and all, but sometimes you'd just rather use a controller and views instead of a page.
share_layouts
That's ok too, because there's a very popular extension called `share_layouts`.
ERb, Haml -> Radiant Layout
share_layouts lets you set a Layout, as in a Radiant Layout, within which your regular ERb or Haml view will render.
content_for :side
part(:side)
Captured content blocks - using `content_for` - become page-parts,
@content_for_layout
part(:body)
and the default content becomes the body part, all of which are rendered in your Radiant Layout via `r:content` tags.
@breadcrumbs@title
You can also set the breadcrumbs and title that will be rendered inside our phantom page.
MOAR contexthttp://icanhascheezburger.com/2007/02/23/moar/
And should that not be good enough, and you need even better context, say, for rendering localized navigation or an inherited sidebar,
endpoints
you can reify these phantom pages as "endpoints" in the page-tree,
“Application”page-type
placing an "Application" page at the root of your controller route.
/widgets
That is, if your controller sits at '/widgets',
“widgets” slug
you'd make a page as a child of the root page with the slug 'widgets'.
just a spoonful of sugar
And that's just a smidgen of how you can customize Radiant.
RadiantRailsRuby
Because it's built on Rails, nearly anything you can do with Ruby and Rails can be done inside Radiant extensions.
workflowlifecycle
You could play with the model workflow and lifecycle,
concurrent_draft(working copies)
like the `concurrent_draft` extension that enables working copies,
scheduler(timed publish)
or the `scheduler` extension that lets you specify dates for your pages to appear and disappear from the site.
multi_site(virtual hosts)
Or you could modify built-in models and controllers, like the `multi_site` extension that creates virtual hosts as multiple page-trees inside the same Radiant instance.
twitterthirty_boxes
You could integrate with a third-party web-service, like the `twitter` and `thirty-boxes` extensions.
file_systemDB <-> files
Or you could make design, development, and maintenance easier like the `file_system` extension, which serializes models into text files,
import_export(big YAML file)
or the `import_export` extension that dumps the whole database to a YAML file.
ext.radiantcms.orgAnd once you've completed your masterpiece, you can share it on the extension registry,
script/extension install
thereby enabling automatic installation scripts for your users.
The world is your oyster.
fin
I hope this has whet your appetite to use Radiant
<r:questions:ask />
do you have any questions?