Content Management with Podius

A presentation for the Open Source Developers Conference OSDC::Israel::2006 and Perl Workshop in Israel, 2007, by Mikhael Goikhman.

See index page.

Applied Content Management

A good content management system may be useful anywhere, from a personal blog to enterprise stores and online magazines.

Podius is an example of a flexible CMS that simplifies building of dynamic web-sites, by providing clean ways to create, manage and publish all kinds of complex content.

This presentation discusses common content management issues, and demonstrates how to setup a simple dynamic shop-like site from scratch.

Application: Online shop

Say, you inherited your family shop and want to catalog all products in the store.


Model: Shop, Product (Book, Computer, Phone), Category.

Application: Online magazine

Or you are a newspaper editor that works non-stop and publishes a new edition daily (and updates it with flash news in real time).


Model: Archive, Edition, Section, Article.

Application: Modern site

These days many sites of geeks or companies are not static, but update their contents and contain some kind of "news" for visitors. The look of such sites is not changed, just the content.


Model: Site, Section, RegularPage, NewsItem.

Creating data model

Designing suitable application object model (bussiness objects) is the most critical step. The model is directly affected by all application requirements, and in the end it dictates the application behaviour and shortcomings.

Possible questions about the model (application specific):

Possible issues about accessing objects (CMS specific):

Solution to the model problems in Podius

Component == business object. Supplied component types:

Components consist of properties. Example of properties:

property nameproperty typearguments
authorScalaroptional default value
is_validBooleanoptional default value
articlesComponentCollectionArticle
scheduleScalarTable[ "start_time", "end_time", "worker_name" ]

Rich set of supplied property types:

Hierarchy of component and property classes. Creating custom classes is easy by inheriting.

Solution for containment in Podius and terminology

All components compose a tree. Every component (except for main_components on level 1 has a parent, so called container. Every component may have any number of children collections.

Each property is owned by its owner component. Each component is part of exactly one ComponentCollection property, owned by the parent component (container).

To implement this, components have two special properties, "container" (a link to its parent component) and "container_collection_name" (the name of the parent property containing this component). These properties are hidden and managed automatically.

Aggregation versus Linkage

Podius: Creating new project

The podius software comes with the core project. Each new project should inherit from the core or any other podius-based project.

% bin/admin/prepare-project OnlineShop
The project's home directory is /data/projects/OnlineShop
Creating project directories  ... done.
Copying initial project files ... done.
Modifying some project files  ... done.
Additional initializations    ... done.
Project OnlineShop is prepared.

% cd ../OnlineShop/

Practical part.

Defining component model

The first thing we want in a new project is to define its model.

The default model looks like:

% bin/admin/list-model
Publishable
  Edition
  Section
  Article

model details...

This is done by creating perl class for each component.

Our goal is this component model:

% bin/admin/list-model
Publishable
  Page
    Center
    Section
    ItemPage
      Article
      Product
  ArticleSet
  User
  UserSet
  Address
    BillingInfo
  ProductSet

model details...

package Podius::Component::Page;

use base 'Podius::Component::Publishable';

sub get_own_property_types {
	return [
		[ title       => 'Scalar' ],
		[ phrase      => 'Scalar' ],
		[ keywords    => 'BigScalar' ],
		[ description => 'BigScalar' ],
	];
}

sub get_name {
	my $self = shift;
	return $self->phrase || $self->name || $self->title;
}

1;
package Podius::Component::ItemPage;

use base 'Podius::Component::Page';

sub get_own_property_types {
	return [
		[ abstract    => 'BigScalar' ],
		[ body        => 'Text' ],
		[ active      => 'Boolean' ],
	];
}

1;
package Podius::Component::Section;

use base 'Podius::Component::Page';

sub get_own_property_types {
	return [
		[ sections    => 'ComponentCollection' , 'Section' ],
		[ products    => 'VComponentCollection', 'Product' ],
		[ articles    => 'VComponentCollection', 'Article' ],
		[ large_image => 'Image', 'images/auto/Section/default1.gif', 'Default Label' ],
		[ small_image => 'Image', 'images/auto/Section/default2.gif', 'Default Label' ],
	];
}

sub get_name {
	my $self = shift;
	return $self->phrase || $self->get_id;
}

1;
package Podius::Component::UserSet;

use base 'Podius::Component::Publishable';

sub get_own_property_types {
	return [
		[ users     => 'ComponentCollection', 'User' ],
		[ user_sets => 'ComponentCollection', 'UserSet' ],
	];
}

1;
package Podius::Component::User;

use base 'Podius::Component::Publishable';

sub get_own_property_types {
	return [
		[ user_name        => 'Scalar' ],
		[ password         => 'Password' ],
		[ first_name       => 'Scalar' ],
		[ middle_name      => 'Scalar' ],
		[ last_name        => 'Scalar' ],
		[ email            => 'Scalar' ],
		[ phone            => 'Scalar' ],
		[ cellular         => 'Scalar' ],
		[ fax              => 'Scalar' ],
		[ addresses        => 'ComponentCollection', 'Address' ],
		[ shipping_address => 'VComponent', 'Address' ],
		[ billing_address  => 'VComponent', 'Address' ],
		[ billing_info     => 'ComponentCollection', 'BillingInfo' ],
		[ order_details    => 'ScalarTable', [ 'Product name', 'Color', 'Flavors', 'Quant', 'Price' ] ],
		[ affiliate_id     => 'Integer' ],
		[ is_validated     => 'Boolean' ],
	];
}

1;

more component classes...

Populating data

Once the model is defined, it is the time for our content editors to create useful content. This may be done using a web interface (podius.cgi).


Changing model of live data

At some point we realize that attaching one order per User was a mistake. Possible steps to refactor our model:

#!/usr/bin/perl -w

use Podius;
use Podius::Shortcuts;

my $cache = create_component_cache();
my $center = $cache->get_component("Center", 1) or die "No Center";

$cache->begin_tx;

# foreach_user is our Podius::Component::Center method
$center->foreach_user(sub {
        my $user = shift;
        my $order = $cache->create_component("Order");
        $order->details($user->order_details);
        $user->orders->add($order);
});   

$cache->commit_tx;

Publishing content

One of the common task of content management systems is publishing the content into different medias. I.e. converting certain samples of the content into appropriate formats, like:

Creating web design (template pages)

Semi-static vus constantly-changed content

Semi-static content

Changed content

Rotated content

Templating systems

There is a large number of templating systems available for any programming language, with a different set of features and syntax.

Recursive syntax:

[[ FOREACH $article @articles
   unmodified_content [[ $article.name ]] unmodified_content
]]

Flat syntax:

[[ FOREACH $article @articles ]]
    unmodified_content [[ $article.name ]] unmodified_content
[[ END FOREACH ]]

Embeding template directives into the text:

Name is <% =$name %>

Embeding text into the template directives:

<% foreach $article (@articles) { print $article->title, "<br>" } %>

Podius: Publishing

Every Publishable component has property "page_names" - a list of pairs (template page name, published page name). This list is initialized from the project configuration, and may be changed or reset.

A component that is mapped directly into at least one web page may better inherit from Page rather than pure Publishable, it adds standard page attributes, like "keywords", "description", "title", "body" and more.

See example for the main Center component.

Neat features:

Template syntax

<{ get_name }>                     # name of the current component

<a href="photos/<{ nick &escape_page_name }>.jpg"><{nick}></a>

<{ container get_name }>           # name of the parent component

<{ container container get_id }>   # id of the grand-parent

<{ has_property('english_name') ? english_name : name &translate }>

<h1><{ title &htmlize }></h1>
<div class="body"><{ body_as_html }></div>

Articles: <ul>
<{articles get_all ({
    <li><a href="<{&link}>"><{title &htmlize}></a>
})}>
</ul>

Creating templates

Let's define 3 published pages for our main Center component:
Template page namePublished page name
generic.htmlindex.html
best_products.txtfeed/best_products.txt
best_articles.txtfeed/best_articles.txt

generic.html is a standard template; other two short templates may look like this:

best_products.txt - feed data of 10 chosen products:

<{
    products get_first(10) ({<{name}>|<{title}>|<{price}>|<{sale_price}>})
}>

best_articles.txt - feed data of chosen articles:

<{
    articles get_all ({<{date_created &format_date}>|<{name}>|<{title &remove_breaks}>})
}>

Static vus dynamic publishing

Static publishing

The recommended practice is to preview before real publishing. To catch all kinds of errors in the data or templates before the content riches the visitors.

Publishing may be done by content editors either using the web GUI or from command line.

bin/admin/show-component-tree                       # show component names and ids
bin/admin/publish-component --recursive Center 1    # publish all components
bin/admin/publish-component --preview Article 2001  # preview one component
bin/admin/publish-component 2001,3001               # publish individual components

Dynamic publishing

A more risky method is to make all data available in real time. This may be done using provided dynapub.cgi. The first "page_names" entry of each component is considered its main page and main static link.

Modern world with Web 2.0

More and more community web sites appear that customize their web pages to match the visitor preferences and even allow visitors to create their own content.

Compromise: static plus dynamic publishing

Apache::ASP is a nice framework to create dynamical sites in perl. Still, if there are thousands or millions of visits daily, building the same content again and again is not optimal. Solution:

Possible CMS applications

  • online magazine
  • online shop
  • numismatic catalog
  • data center
  • dating service
  • narrow-topic or wide-open communities
  • wiki
  • blob
  • what not...

References