Content Management That Won't Rot Your Brain

Preview:

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?

Recommended