Intermix Bricolage and Catalyst::View::Mason

Remember the calendar entry about delivering "semi static" files from a CMS like Bricolage (http://catalyst.perl.org/calendar/2007/17)? Today I show another special feature combination that combines Bricolage with a Catalyst application.

I want to use pages generated from Bricolage as page frames around minimalistic templates in my Catalyst application.

Intro

Mason

I use HTML::Mason as my preferred template language.

Mason provides a nice mechanism called autohandler that can combine several layers around an inner template depending on the directory structure. I use that very often to create general html page frame at top level, adding primary navigation on next subdir, then secondary navigation, then the plain inner content. Think of Matryoshka dolls (http://en.wikipedia.org/wiki/Matrjoschka).

Mason in Bricolage and Catalyst

Creating sophisticated templates is the main focus in a Bricolage project. When I augment a mostly static website with dynamic pages from Catalyst application I want to re-use all the template logic instead of maintaining a second set of autohandlers and components for the Catalyst application.

The goal

I want to take an existing page from Bricolage and use its frame with all navigation bars, breadcrumbs and other context, but replace the inner content with the minimalistic template from Catalyst.

Bricolage "Output Channels"

"Output channels" are the mechanism of Bricolage to render one content element in different ways, e.g, for language variants or different markup variants like HTML/XML/TXT. But other, even strange ideas are possible.

You can either apply a completely different template per output channel or re-use the template from the primary output channel and conditionally handle special situations. The latter is what we need here.

autohandler

An autohandler template in Bricolage could look like this:

 <!DOCTYPE html ... >
 <html ... >
 <head>
   <& /html_meta.mc &>
 </head>
 <body>
   <& /navigation.mc &>
   ...

 % if ( $burner->get_mode == PUBLISH_MODE &&
 %      $burner->get_oc->get_name eq "myapp_Pageframe" )
 % {
     <!-- __pageframe_content__ -->
 % } else {
 %   $m->call_next;
 % }

   ...
   <& /footer.mc &>
 </body>
 </html>

This is the outline of a page, with html head, beginning body, navigation stuff and when it comes to the inner area of the page, it specially handles the output channel myapp_Pageframe to just generate a html comment that we will use as placeholder in our Catalyst application, else it calls the next "Russian doll" (call_next), meaning the normal CMS content is rendered there.

Output channel

The used output channel myapp_Pageframe is configured in Bricolage to generate a file pageframe.html in the same path as the default output channel file index.html.

Both files are basically the same, only the inner content is either really there or just a placeholder. This way I can have a page frame who always provides the same look as its corresponding content page.

You can apply this output channel to single stories or to all stories of the same type. I apply it only to single stories.

Catalyst controller root

In my Catalyst application I want to declare such a page frame file, additionally to the normal template.

To have it everywhere available I placed the following action in Catalyst::Controller::Root. It completes a typical Bricolage story URI into a complete file path and puts this into the stash.

 package MyApp::Controller::Root;
 use base 'Catalyst::Controller::BindLex';

 sub set_pageframe : Private {
   my ( $self, $c, $story_uri ) = @_;

   my $pageframe : Stash =
     '/home/renormalist/myapp/myappsite'     # DocumentRoot
     .$story_uri
     . "/pageframe.html";
 }




Catalyst controller

I typically have one controller with all of its actions corresponding to one same page frame. Therefore I declare the needed pageframe from Bricolage using auto:

 package MyApp::Controller::App::News::Daily;

 # Set frame template to use from CMS
 sub auto : Private {
   my ( $self, $c ) = @_;
   $c->forward ('/set_pageframe', [ '/news/daily' ]);
 }

That means all templates that are used by MyApp::Controller::App::News::Daily should be wrapped by the page frame corresponding to the /news/daily story in Bricolage .

Catalyst view

My Catalyst::View::Mason now should create a temporary template that consists of my original template embedded into the given page frame where the placeholder, well, holds the place.

This is done by overwriting the get_component_path method from Catalyst::View::Mason in MyApp::View::Mason.

It takes the original component path to find the template, merges it with the page frame into a temporary file and gives back that temporary filename instead of the original template path.

Here are the important lines from MyApp::View::Mason:

 package MyApp::View::Mason;
 use base 'Catalyst::View::Mason';

 use File::Temp qw/ tempfile tempdir /;
 use File::Slurp qw/ slurp /;
 use File::Basename;

 our $re_placehalter    = qr/<!-- \s* __pageframe_content__ \s* -->/x;
 our $tmpdir            = "/tmp/myapp_view_mason";
 __PACKAGE__->config(
   comp_root => [
                 [ myapproot => MyApp->config->{root}.'' ], # stringify
                 [ docroot   => '/home/renormalist/myapp/myappsite' ],
                 [ merged    => $tmpdir ],
                ]
 );

In the comp_root config the merged entry is the one where our merged files are written to.

 sub get_component_path {
     my ($self, $c) = (shift, @_);

     my $component_path = $self->SUPER::get_component_path ( @_ );
     return $component_path unless $c->stash->{pageframe};

     my $pageframe             = $c->stash->{pageframe};
     my $merged_component_path = $self->embed_template_into_pageframe(
                                   $c,
                                   $component_path,
                                   $pageframe
                                 );
     return $merged_component_path;
 }

The sub doing the real work is this:

 # returns a filename of a combined template
 sub embed_template_into_pageframe {
     my ($self, $c, $component_path, $pageframe) = (shift, @_);

     my $template = $c->config->{root} . "/$component_path";

     return $component_path unless -e $pageframe; # original template

     my ($mergedfh, $mergedfile) = tempfile("XXXXXXXX", DIR => $tmpdir);

     # slurp original template and pageframe
     my $template_content  = slurp( $template  );
     my $pageframe_content = slurp( $pageframe );

     # create a merged component
     $pageframe_content =~ s/$re_placeholder/$template_content/msg;
     print $mergedfh $pageframe_content;

     return basename $mergedfile;
 }

This is slightly simplified for the article, to make the principle obvious. It creates a lot of temp files and is probably not the fastest way.

In my project I don't use File::Temp but a predictable name derived from the original template and the pageframe, solving the problem with lots of temporary files and a caching mechanism at once.

That's it

Thanks to rafl for maintaining and accepting some refactoring in Catalyst::View::Mason that allowed the described mechanism in the first place.

Author

Steffen "renormalist" Schwigon

http://renormalist.net

See also

* http://bricolage.cc - Bricolage, statically publishing, enterprise-class CMS, written in Perl
* http://masonhq.com - HTML::Mason head quarter.