Content Management That Won't Rot Your Brain


Published on

Published in: Technology
No Downloads
Total views
On SlideShare
From Embeds
Number of Embeds
Embeds 0
No embeds

No notes for slide

Content Management That Won't Rot Your Brain

  1. Content Management That Won’t Rot Your Brain Sean Cribbs
  2. complicated Content management on the web can be pretty complicated.
  3. hundreds There are hundreds of open-source content management systems to choose from,
  4. in-completion all in various states of completion.
  5. un-intuitive Most of them have unintuitive user interfaces,
  6. tangled tangled content models,
  7. ugly code and opaque, poorly-architected and undocumented code that refuses to yield to customization attempts.
  8. content model These include abominations like ezPublish, which is so in love with its content model
  9. content model that it seems they implemented the editing interface in the content model.
  10. huh? Good luck figuring that one out.
  11. pluggable Or Xaraya, that despite its ultra-pluggable code modules with proscribed interfaces
  12. bridge to nowhere doesn't seem to do anything it advertises.
  13. template surgery Or Joomla & Mambo, which require template surgery to create a custom look and feel
  14. everything to everyone while trying to be everything to everyone.
  15. FTP is so 1971 Why is it so hard to publish a web page that I don't have to use FTP to edit?
  16. web standards Why can't I use HTML, CSS and the other web standards I know and love?
  17. ideal CMS So what would I want in my ideal CMS?
  18. #1 : simple First, it would be simple to use and understand.
  19. meta is overrated I'm not impressed with hyper-abstract meta-content models, just make it work.
  20. #2 : standards markup Second, it would let me write markup and presentation in any format and structure I like,
  21. design freedom giving me the freedom to design my site the way I want.
  22. #3 : powerful tools Third, it would have simple and powerful tools built-in to make most content-generation tasks a breeze,
  23. PHP not required without needing plugin modules.
  24. #4 : clear code Fourth, it would have a well-architected code design
  25. easy customization so I could understand and customize it easily.
  26. drum roll please Fortunately, there's a CMS that lets me do all these things.
  27. It’s Radiant! It's called Radiant, and it's written in our favorite language, Ruby.
  28. APP DHH ROV ED And yes, even David likes it.
  29. #1 : simple content model So how does Radiant meet my expectations? First, Radiant's content model is easy to understand.
  30. Pages Every Radiant site is made up primarily of Pages,
  31. acts_as_tree which are arranged in a tree, much like a directory/file structure.
  32. has_many :parts Pages have many content pieces called parts,
  33. logical pieces which let you break your pages into logical sections
  34. content_for think quot;content_forquot; blocks in Rails views.
  35. Page-Types (STI) Any page can have a special type, essentially a subclass of the Page model,
  36. page-level plugins that acts as a localized plugin, modifying the front-end behavior of that page.
  37. Layouts Pages also have layouts,
  38. design details which let you abstract common design details, just like Rails layouts,
  39. Content-Type: text/html and also let you specify the content type - be it HTML
  40. Content-Type: text/xml XML
  41. Content-Type: text/css CSS, or whatever.
  42. Snippets To round out the system are Snippets,
  43. render :partial which are kind of like Rails partials
  44. repetitive small pieces of common or repetitive content
  45. contextual that can be rendered in the context of many pages.
  46. Filters Pages and snippets can use text filters
  47. Textile Markdown etc. that let you write content in shortcut languages like Textile or Markdown.
  48. template language This is all tied together with a template language
  49. Radius called Radius
  50. non-evaluating that keeps code well-separated from your content.
  51. #2 : complete design control Second, Radiant lets you design your site the way you want.
  52. any text It doesn't care whether you're publishing HTML, CSS, Javascript, plain text or whatever.
  53. (no binary) You probably don't want to put binary data in a page, but any kind of text is OK.
  54. 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;
  55. write your own markup you can write the markup yourself.
  56. #3 : powerful tools Third, Radiant has powerful tools
  57. over 50 tags about 50 built-in template tags to help you accomplish many content-generation tasks.
  58. navigation, sucker You can use tags to generate navigation
  59. row-striping stripe table rows
  60. feed me, Seymor collate content into a feed
  61. no logo for you! or display the big company logo only on the homepage.
  62. #4 : clean code Last but not least, our reason for being here
  63. well-architected Radiant is well-architected
  64. developer goodies and has plenty of goodies for developers.
  65. 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.
  66. Extensions To this end, Radiant has a souped-up plugin system called quot;extensionsquot;
  67. plugins + awesome which picks up where Rails plugins leave o.
  68. merb-slices Extensions are more like `merb-slices`
  69. Rails Engines or Rails Engines than plugins.
  70. app/controllers app/helpers app/models app/views An extension can have its own quot;appquot; folder with controllers, helpers, models and views.
  71. db/migrate An extension can define database migrations
  72. public public files
  73. vendor/plugins and even its own plugins.
  74. TATFT Most importantly, you can quot;test all the timequot; in your extensions
  75. RSpec because Radiant provides a full RSpec harness
  76. dataset :pages a bunch of test datasets
  77. @page.should render(“foo”) and some nifty matchers to help out with Radiant-specific stu.
  78. class FooExtension Radiant::Extension In addition to the directory and file structure, every extension is expressed as a singleton class
  79. def activate that can provide some startup code in the quot;activatequot; method
  80. define_routes do |map| map.resources :fuzzy_bears end route definitions
  81. description “My first extension.” url “” and some metadata that is displayed in the user-interface.
  82. TIMTOWTDI Depending on the scenario, there are a number of dierent ways we can customize Radiant to accomplish our goals, all of which can be nicely packaged up in extensions.
  83. 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.
  84. #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,
  85. r:awesome / through the use of these 'r'-prefixed XML-like tags.
  86. (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.
  87. r: Radius tags are composed of four parts - the 'r' prefix;
  88. r:children:each the tag name, which may be colon separated;
  89. r:children:each order=“desc” the attributes;
  90. 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.
  91. 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.
  92. 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.
  93. 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.
  94. module LazyTags First, we'll make a module in our extension to hold the tag definition.
  95. include Radiant::Taggable Include quot;Radiant::Taggablequot; and we're ready to start defining tags.
  96. tag ‘stylesheet’ do |tag| Next we'll start our tag definition with the `tag` keyword, the tag name, and a block yielding one parameter.
  97. url = tag.attr[‘url’] Inside our tag definition block, we'll grab the 'url' attribute o the tag,
  98. %Q[link rel=”Stylesheet” type=”text/css” href=”#{url}” /] and interpolate that into the proper place in the HTML `link` tag.
  99. end The return value of the tag-definition block is what is rendered, so we're done.
  100. def activate Page.send :include, LazyTags end 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.
  101. |tag| The yielded 'tag' object inside a definition is very powerful.
  102. 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.
  103. tag.globals everywhere Its primary properties are 'globals', which gives you access to the global environment, including the current page being rendered;
  104. tag.locals cascading, contextual ‘locals' which is a cascading symbol-table of sorts, contextual to the current parsing environment;
  105. tag.attr Hash 'attr', a hash of attributes placed on the current tag;
  106. tag.double? r:footext/r:foo 'double?', which is true when the tag contains other tags or text,
  107. tag.single? r:bar / and 'single?' which is true when the tag is self-closing.
  108. 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.
  109. do |child| = child tag.expand end.join This is essentially what the `r:children:each` tag does - iterate on each of the children, changing `` on each iteration, then joining the output. To trigger rendering of a tag's contents, we just call `tag.expand`.
  110. 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.
  111. tag.block One neat trick is capture the contents of a container tag and reuse it in multiple places with `tag.block`.
  112. 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.
  113. tag.locals.hash = {} tag.expand # ... First, it sets up a hash that can be used inside nested tags, then renders its contents.
  114. tag ‘navigation:here’ do |tag| tag.locals.hash[:here] = tag.block end The contained quot;navigation:herequot; tag assigns its block into the created hash.
  115. # for each link... if url == page.url tag.locals.hash[:here].call end Then the quot;navigationquot; tag calls the quot;herequot; 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.
  116. #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.
  117. class Widget ActiveRecord::Base Let's say you've built a model for widgets
  118. r:widgets / and some Radius tags to display them in a page,
  119. CRUD and now you need to create an interface for CRUDing them.
  120. map.resources :widgets The first thing you might want to do, after creating your controller,
  121. Add “Widgets” Tab is to add a tab to the interface so your widgets are navigable.
  122. Extension.admin Radiant exposes an quot;adminquot; object to every extension that lets you tweak the interface
  123. admin.tabs.add “Widgets”, “/admin/widgets” and easily add tabs. As well as letting you simply add tabs,
  124. :before = “Layouts” you can also specify where they occur in the tab order
  125. :visibility = [:admin] and who can see them. But that's just a simple case of how to customize the UI.
  126. a dash of spice Often what you want to do is just add a little piece to one view or another.
  127. widget list Say you need to be able to see your widget list while editing a page.
  128. fine-grained regions Luckily, you can inject a view partial into any number of defined quot;regionsquot; in the view template you want to modify, without overwriting the whole thing. This is enabled again through the admin object.
  129. app/views/admin/pages/ _widget_list.erb To add our widget list, we just put it in the app/views/admin/pages directory,
  130. 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.
  131. 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.
  132. brute-force override If you don't like the finessed approach, you can brute-force override any view template,
  133. application.html.haml including the default layout,
  134. extensions before core by putting one of the same name in your extension, since extension view paths are searched before Radiant's built-in paths.
  135. #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.
  136. page rendering Let's take a look at the process of how a page is rendered.
  137. map.connect “*url” First, any request URL that doesn't match an admin or extension controller gets sent to the splatted route
  138. SiteController#show_page assigned to SiteController.
  139. request.get? or request.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.
  140. show_uncached_page(url) Otherwise, it tries to show an uncached version of the page.
  141. @page = find_page(url) The controller tries to find the page by the given URL,
  142. process_page(@page) and then processes it if found
  143. @page.process(request, response) calling page.process with the request and response
  144. @cache.cache_response(url, response) caching the response where appropriate.
  145. render :template = “site/not_found”, :status = 404 If it's not found, you get the default 404 page,
  146. redirect_to welcome_path and if there's no pages at all in the database, you get redirected to the login screen.
  147. SiteController#find_page So within this process are a number of extension points to play with - `find_page`,
  148. SiteController#process_page `process_page`,
  149. Page#process(req, resp) and `Page#process`.
  150. 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.
  151. choose a Page class Since when we create a new page in the interface, Radiant lets us choose the page class we want,
  152. redirect oldies let's create a new Page class that performs a redirect from the old URL to our new location.
  153. class RedirectPage Page So we'll choose to override the `process` method in our new subclass that we'll call `RedirectPage`.
  154. page.part(:body).content == “/old/url” We'll assume that we put the new URL in the 'body' part of the page,
  155. 301 Moved Permanently and we'll want to consider this a permanent redirection.
  156. def process(req, res) url = parse_object part(:body) To accomplish this, inside we'll just render the body part to get our URL,
  157. res.redirect url, “301 Moved Permanently” end then cause the response to redirect.
  158. def cache?; true; end Believe it or not, a redirect can be cached!
  159. 5-minute cache with 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.
  160. 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.
  161. POST PUT DELETE don’t cache This means that when a page receives POST, PUT, or DELETE, you can do cool stu with the data in the request.
  162. mailer database_form For example, the mailer and database_form extensions respectively process email forms and store data to the database on POST requests,
  163. seamless experience allowing a seamless front-end experience.
  164. #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.
  165. share_layouts That's ok too, because there's a very popular extension called `share_layouts`.
  166. 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.
  167. content_for :side part(:side) Captured content blocks - using `content_for` - become page-parts,
  168. @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.
  169. @breadcrumbs @title You can also set the breadcrumbs and title that will be rendered inside our phantom page.
  170. MOAR context And should that not be good enough, and you need even better context, say, for rendering localized navigation or an inherited sidebar,
  171. endpoints you can reify these phantom pages as quot;endpointsquot; in the page-tree,
  172. “Application” page-type placing an quot;Applicationquot; page at the root of your controller route.
  173. /widgets That is, if your controller sits at '/widgets',
  174. “widgets” slug you'd make a page as a child of the root page with the slug 'widgets'.
  175. just a spoonful of sugar And that's just a smidgen of how you can customize Radiant.
  176. Radiant Rails Ruby Because it's built on Rails, nearly anything you can do with Ruby and Rails can be done inside Radiant extensions.
  177. workflow lifecycle You could play with the model workflow and lifecycle,
  178. concurrent_draft (working copies) like the `concurrent_draft` extension that enables working copies,
  179. scheduler (timed publish) or the `scheduler` extension that lets you specify dates for your pages to appear and disappear from the site.
  180. 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.
  181. twitter thirty_boxes You could integrate with a third-party web-service, like the `twitter` and `thirty-boxes` extensions.
  182. file_system DB - files Or you could make design, development, and maintenance easier like the `file_system` extension, which serializes models into text files,
  183. import_export (big YAML file) or the `import_export` extension that dumps the whole database to a YAML file.
  184. And once you've completed your masterpiece, you can share it on the extension registry,
  185. script/extension install thereby enabling automatic installation scripts for your users.
  186. The world is your oyster.
  187. fin I hope this has whet your appetite to use Radiant
  188. r:questions:ask / do you have any questions?