App::Office::CMS - The Canny, Microlight and Simple CMS


The scripts discussed here, cms.cgi and cms.psgi, are shipped with this module.

A classic CGI script, cms.cgi:

        #!/usr/bin/env perl

        use strict;
        use warnings;

        use CGI;
        use CGI::Application::Dispatch;

        # ---------------------

        my($cgi) = CGI -> new;

        CGI::Application::Dispatch -> dispatch
                args_to_new => {QUERY => $cgi},
                prefix      => 'App::Office::CMS::Controller',
                table       =>
                ''              => {app => 'Initialize', rm => 'display'},
                ':app'          => {rm => 'display'},
                ':app/:rm/:id?' => {},

A Plack script, cms.psgi:

        #!/usr/bin/env perl

        use strict;
        use warnings;

        use CGI::Application::Dispatch::PSGI;

        use Plack::Builder;

        # ---------------------

        my($app) = CGI::Application::Dispatch -> as_psgi
                prefix => 'App::Office::CMS::Controller',
                table  =>
                ''              => {app => 'Initialize', rm => 'display'},
                ':app'          => {rm => 'display'},
                ':app/:rm/:id?' => {},

                enable "Static",
                path => qr!^/(assets|favicon|yui)/!,
                root => '/var/www';

For more on Plack, see my intro to Plack:


App::Office::CMS is the Canny, Microlight and Simple Content Management System.

o Canny

The canniness comes in observing that almost all web sites have a menu for navigation.

So, this module gives you (the content manager) a menu designer.

The idea is that the menu you design is 'live', so you use the web site's own menu to design both the menu itself and the whole web site.

o Microlight

This means use Moo rather than Moose.

For the same reason, DBIx::Simple rather than DBIx::Class.

And hence Brannigan rather than Data::Verifier, because the latter uses Moose::Util::TypeConstraints, and hence a whole slew of Moose::Meta::* modules.

Lastly, I'm not claiming this package is as small as it could possibly be. For example, I could replace Text::Xslate with HTML::Template to shrink it, but I don't want to do that.

o Simple

The other motivation is to deliver an application which anyone, with minimal training, can master.


Up to V 1.00, 99.99% of the effort went into the structure of the code, i.e. the mangement of a web site, and not into supporting content generation.

That's why the features provided for generating content are minimalistic.

The end-user docs are discussed below, under FAQ, and are strongly recommended reading.

This is a simplified version:

o Multiple web sites

You can create any number of web sites.

o Multiple designs per web site

Each web site can have any number of designs.

Every design has a menu of web pages.

o Multiple web pages per design

As expected, each design consists of any number of web pages.

o Auto-generation of web pages and assets.

When you create a new site and design, a default web page is created.

This also happens if you change the name of a web site or of a design, and click Save.

This web page has a default asset (output file template), and default (empty) content.

o Why is the name of the homepage in the config file?

To make it easy to edit!

Also note that there are 2 template files shipped with this distro, one for the homepage and one for any other page. These templates are not fancy, they are just there as guidelines for how you should develop your own templates.

See htdocs/assets/templates/app/office/cms/page.templates/*.tx for the templates.

See also data/asset_types.txt for the other place template information is stored.

o Web page attributes

So far, each web page is given a small number of attributes, including the template to be used to create the corresponding output web page (at generation time).

Using the Javascript editor, you create the content for each page.

o Output

Web pages are generated by pouring the content into the template.

Two (2) sample templates are provided.

o Configuration

See lib/App/Office/CMS/.htoffice.cms.conf.

o Errors

As far as possible, Try::Tiny is used to catch non-DBI errors.

DBI errors are caught using the HandleError attribute key in the call to DBI's connect() method.


Minimal effort has been made to sanitize error messages, so there's a risk that information you don't wish to leak out may be displayed on the end-user's screen.

Feel free to recommend changes in this area.

CGI form field data is passed thru CGI::Untaint and, optionally, HTML::Defang.


This module is available as a Unix-style distro (*.tgz).

See for help on unpacking and installing distros.

Installation Pre-requisites

The Yahoo User Interface (YUI)

This module does not ship with YUI. You can get it from:

All development was done using V 2.8.1.

Currently, I have no plans to port this code to V 3 of YUI.

See also lib/App/Office/CMS/.htoffice.cms.conf, where it specifies the URL used by the code to find YUI's JavaScript files.

The output templates use these 5 YUI files:

o CSS: yui/build/treeview/assets/skins/sam/treeview.css
o JS: yui/build/yahoo/yahoo-min.js
o JS: yui/build/dom/dom-min.js
o JS: yui/build/event/event-min.js
o JS: yui/build/treeview/treeview.js

More on Configuration

At various places I refer to a file, lib/App/Office/CMS/.htoffice.cms.conf, shipped in this distro.

Please realize that if you edit this file, you must ensure the copy you are editing is the one used by the code at run-time.

After a module such as this is installed, the code will look for that file in the directory where Build.PL or Makefile.PL has installed the code.

The module which reads the file is App::Office::CMS::Util::Config.

Both Build.PL or Makefile.PL install .htoffice.cms.conf along with the Perl modules.

So, if you unpack the distro and edit the file within the unpacked code, you'll still need to copy the patched version into the installed code's directory structure.

There is no need to restart your web server after updating this file.

Creating the database

OK, here we go...

I use SQLite because it's shipped with recent versions of Perl.

Running scripts/ (below) will create the database.

If you use Postgres, do this to create the database:

        shell>psql -U postgres
        psql>create role cms login password 'cms';
        psql>create database cms owner cms encoding 'UTF8';

Creating and populating the database tables

The distro contains a set of text files which are used to populate constant tables. All such data is in the data/ directory.

This data is loaded into the database using programs in the distro. All such programs are in the scripts/ directory.

After unpacking the distro, create and populate the database thus:

        shell>cd App-Office-CMS-1.00
        # Naturally, you only drop /pre-existing/ tables :-),
        # so use later, when re-building the db.
        #shell>perl -Ilib scripts/ -v
        shell>perl -Ilib scripts/ -v
        shell>perl -Ilib scripts/ -v
        shell>perl -Ilib scripts/ -v

See also scripts/

Note: The '-Ilib' means 2 things:

o Perl looks in the current directory structure for the modules

That is, Perl does not use the installed version of the code, if any.

o The code looks in the current directory structure for .htoffice.cms.conf

That is, it does not use the installed version of this file, if any.

So, if you leave out the '-Ilib', Perl will use the version of the code which has been previously installed, and then the code will look in the same place for .htoffice.cms.conf.

Installing the module

Install App::Office::CMS as you would for any Perl module:


        cpanm App::Office::CMS

or run:

        sudo cpan App::Office::CMS

or unpack the distro, and then either:

        perl Build.PL
        ./Build test
        sudo ./Build install


        perl Makefile.PL
        make (or dmake)
        make test
        make install

Either way, you need to install all the other files which are shipped in the distro.

Install the Text::Xslate (HTML and Javascript) template files

Copy the distro's htdocs/assets/ directory to your web server's doc root.

Specifically, my doc root is /dev/shm/html, so I end up with /dev/shm/html/assets/.

/dev/shm is Debian's RAM disk. Your doc root might be /var/www, or even /var/www/html.

Install the FAQ web page

This FAQ is for using App::Office::CMS via its CGI scripts, not for the generated web site.

In lib/App/Office/CMS/.htoffice.cms.conf there is a line:


This page is displayed when the user clicks FAQ on the About tab.

A sample page is shipped in docs/html/ It has been built from docs/pod/ (by running a script I wrote,, which in turn is a simple wrapper around Pod::Simple::HTML).

So, copy the sample HTML file into your web server's doc root, or generate another version of the page, using docs/pod/cms.faq.pod as input.

Install the trivial CGI script and the Plack script

Copy the distro's httpd/cgi-bin/office/ directory to your web server's cgi-bin/ directory, and make cms.cgi executable.

If I used Apache, my cgi-bin/ dir would be /usr/lib/cgi-bin/, so I would end up with /usr/lib/cgi-bin/office/cms.cgi.

Actually, I run nginx (Engine X), which does not serve CGI scripts, and tiny (thttpd), which does.

See for patching tiny.

See also

Start testing


        starman -l --workers 1 httpd/cgi-bin/office/cms.psgi &

Or, for good debug output:

        plackup -l httpd/cgi-bin/office/cms.psgi &

Or, install cms.cgi and point your browser at:

The Generate Button

Mostly, you'll be working on the Edit Content tab, so that's where the [Generate] button is.

Clicking [Generate] generates all of the pages in the current design. You do not have to be editing content for the homepage to use the [Generate] button.

The disk directory structure created matches the tree structure you have created via the Edit Pages tab.

So, if you created a page called Offices, and under that 2 children called Locations and Staff, then these files will be output:

o offices.html
o offices/locations.html
o offices/staff.html

There are various things to note:

o Case: File and Directory names are output in lower case

This conversion is done with String::Dirify.

o Hierarchy: Child nodes are in a sub-directory under their parent

This structure is generated by tracing the ancestors of each page up the tree of pages, to the root (but excluding the root), and using the parent, grand-parent, etc, names as directory names.

o Root directory

When a site is created, you specify an output directory, and the directories mentioned in the previous items are created under the site's directory.

So, if this site's output directory was in Debian's RAM disk at /dev/shm/html, then the output files' full paths would be:

o /dev/shm/html/offices.html
o /dev/shm/html/offices/locations.html
o /dev/shm/html/offices/staff.html

Path::Class is used to construct these paths in an OS-independent manner, which means you must provide the site's output directory in a format suitable for your OS.


o This module doesn't really create entire web sites!

True, but since it's Open Source, you can extend it yourself.

o But why don't you add millions of features!

I can give you 3 reasons for that: Canny, Microlight and Simple.

o Can I have 2 pages with the same name?

No. When user input is being checked, the page is searched for by name, and a match means the new input is an update for the existing page.

o How is the code structured?

MVC (Model-View-Controller).

The sample scripts cms.cgi and cms.psgi use

        prefix => 'App::Office::CMS::Controller'

so the files in lib/App/Office/CMS/Controller are the modules which are run to respond to http requests.

Files in lib/App/Office/CMS/View implement views, and those in lib/App/Office/CMS/Database implement the model.

Files in lib/App/Office/CMS/Util are a mixture:


This is used by all code.


This is just used to create tables, populate them, and drop them.

Hence it won't be used by CGI scripts, unless you write such a script yourself.


This simplifies logging by allowing me to say:

        $self -> log(debug => 'A message');
        $self -> log(info  => 'Another message');

This is used to validate CGI form data.

o What's the database schema?

See docs/cms.schema.png.

The file was created with scripts/, which uses

That program is a version of, which ships with GraphViz::DBI.

o Does the database server have pre-requisites?

The code is DBI-based, of course.

Also, the code assumes the database server supports $dbh -> last_insert_id(undef, undef, $table_name, undef).

o How do I back up the database?

See the config file .htoffice.cms.conf:

        backup_command = pg_dump -U cms cms
        backup_file = /tmp/pg.cms.backup.dat

When backup_command has a value, the Edit Contents tab gets a [Backup] button, and when this button is clicked:

o The command is run
o STDOUT and STDERR are captured
o If STDERR contains anything, the program exits
o Otherwise, STDOUT is written to the output file

So, why are there 2 lines, and not something like 'pg_dump -U cms cms > /tmp/pg.cms.backup.dat'?

Because I use Capture::Tiny, which does not want you to use redirection.

Lastly, the output is written using File::Slurper.

o What's this thing called 'context' in the menus and pages tables?

It's the value ("$site_id/$design_id") which ties those 2 tables together, just like a foreign key.

o What are these fields menu_orientation_id and os_type_id in the designs table?

I originally allowed the user to select a horizontal or vertical menu format, when generating pages.

The vertical menu is just the tree I ended up with.

I decided not to support horizontal menus, at least in the short term, because of the width of such a menu when the page names became long. It would have appeared just above the site map on the Edit Pages tab, and would have been clickable in the same way the site map tree is.

The os_type_id is for the unsupported code which generates pages in a directory structure for an OS different from the one the code is running on.

You can ignore these 2 fields, and the other 2 tables, menu_orientations and os_types.

o I added a field to update_page_form, but it's data vanishes.

Basdically, copy the code dealing with 'homepage'.

At the very least, ensure you've updated:

o App::Office::CMS::Util::Create.create_pages_table()
o App::Office::CMS::Database.build_default_page()
o App::Office::CMS::Controller::Page
o build_page_hash()
o build_success_result()
o check_page_name()
o update()
o App::Office::CMS::Util::Validator.validate_page()
o App::Office::CMS::Database::Page.save_page_record()
o App::Office::CMS::View::Page.build_update_page_html()

For site data, start with App::Office::CMS::Controller.build_site_hash().

o How to I replace the fundamental editing functionality?


o The 'Edit design' button

This is in site.tx. It's id is submit_edit_design. Leave it as is.

o The 'Edit design' Javascript

Clicking [Edit design] triggers a call to update_site_onsubmit(). This is in site.js.

o Calling the update_page_callback() function (1 of 2)

This is called from within update_site_onsubmit(). The call is in site.js.

The path info is 'page/edit', which is added to the base '$form_action/'.

As per CGI::Application::Dispatch, this calls App::Office::Controller::Page's edit() method.

Change this path info to call a different module. So, 'custom_page/my_edit' will call App::Office::Controller::CustomPage's my_edit() method.


You write *, based (obviously) on *

You can still let *::View::Page process your updated page.tx and page.js templates. See below for details.


This is App::Office::CMS::Util::Validator. You'll have to edit the validate_design() method.

o The declaration of update_page_callback() (2 of 2)

This is in page.js. This is the Javascript function which receives the output of

Replace the code in this function as needed.

o *::View::Page

You'll need to patch the build_head_js() and build_update_page_html() methods in this class.

o page.tx

This is populated by code in the *::View::Page::build_update_page_html() method.

o page.js

This is populated by code in *::View::Page::build_head_js() method.

o Site attributes 'v' Design attributes

When a site/design is submitted for saving, some attributes are saved with the site, and some with the design.

If writing to the database fails, it's probably because the attributes for the design were not copied from the CGI form fields.

Check App::Office::CMS::Database::Design's site2design() method.

o Please explain the program, text file, and database table names

Programs are shipped in scripts/, and data files in data/.

I prefer to use '.' to separate words in the names of programs.

However, for database table names, I use '_' in case '.' would case problems.

Programs such as and, use table names for their data files' names. Hence the '_' in the names of their data files.

o Will you re-write it to use a different Javascript library?

No, that would be an unproductive use of my time.

Other such libraries might do a good job, but I don't believe they'll do a better job.

I have published a review of various Javascript libraries [1], and IMHO YUI is the best.


o How do I change the generated page formats?

The templates used for generating pages are found via the config option page_template_path.

All *.tx files in this directory are put into the page template menu on the 'Update Design' page.

The default homepage template is

The default generic page template is

o Why didn't you use accessors::classic/Class::Accessor/Object::Tiny/...?

For various reasons:

o accessors::classic ...

... does not parse the parameters to new().

o I wanted ...

... to have BUILD() (or even init() ) called after new().

o Class::Accessor::Constructor ...

... will call init(), but it also drags in another set of dependencies.

o Object::Tiny ...

... does not have attribute setters.

o With Mouse, ....

... a syntax-friendly upgrade path the Moose is preserved.

o Will you adopt DBIx::Connector?


o Do I need Apache?

No. Starman, part of Perl's Plack project, is recommended.

See the sample code in httpd/cgi-bin/office.

o What other CMS's are there?


o Lastly, a plea

With your co-operation, I'd like to reserve the namespace App::Office::CMS, and perhaps even App::Office::Wiki, for my own code.

Of course, you're much better off using a TiddlyWiki than waiting for me to start writing a wiki.

For massive wikiness (as distinct from wickedness :-) I draw your attention to both Silki and


o Adopt Git::Repository for versioned backup
o Clean up error handling

For example, when build_error_result is called, rather than build_success_result, the data sent to Javascript must be handled slightly differently.

This includes HandleError in DBI's connect() attributes.

o Make asset handling more sophisticated
o Add begin/end transaction
o Probably need Javascript hash for menu item <-> id

This would allow the client to pass the menu item's id to the server, instead of the text

o Need to document handling of &amp;
o Do we need separate editor windows for each page's head and body?
o How will we handle moving sub-menus?

We don't.

o Ship with SQLite activated, not Postgres
o Consider using <span> instead of <div>
o Auto-generate a site
o Auto-generate a design for a site
o Enhance New Site tab with an Edit Site button

This saves the user the effort of going to the Search tab to find a site or design

o When clicking on the site map, the Edit Pages fields are updated, but the Edit Content fields are not
o Add an option, perhaps, to escape entities when inputting HTML
o Adopt DBIx::Connector
o Implement user-initiated backup and restore
o Change class hierarchy

This is so View does not have to pass so many parameters to its 'has-a' attributes

o Adopt CGI::Untaint::html or HTML::Defang

Considered and rejected: HTML::Sanitizer, HTML::Scrubber.

o Test CGI::Untaint as to its handling of <script>...</script>
o Investigate Quicki's revision system



Email the author, or log a bug on RT:


App::Office::CMS was written by Ron Savage <> in 2010.



Australian copyright (c) 2010, Ron Savage.

        All Programs of mine are 'OSI Certified Open Source Software';
        you can redistribute them and/or modify them under the terms of
        The Perl License, a copy of which is available at: