Serving Templated Directories from a Catalyst Controller Role

One of my web projects has several directories which contain, essentially,
static files with a little bit of dynamic content in the wrapper. This dynamic
content is specific to each user—a change in navigation, some
customization.

The prototype of this project split the responsibility for serving the site
between the frontend web server and the backend Plack server. As you can imagine, it worked
well enough for a prototype, but it clearly needed replacement once the project
became serious.

I didn't want to invest a lot of effort into writing a bunch of unique
controllers for each directory with templated files (/about,
/contact, /resources, et cetera), but I want to share
behavior between all of these routes.

Essentially I have several subdirectories in my templates/
directory which correspond to these routes. Each subdirectory contains one or
more templates for each resource within that route, such as
/about/jobs and /contact/press. Something in my Catalyst application needs to be able
to resolve those requests to those templates.

That's easy enough.

I ended up factoring this code into a parametric role to apply to
controllers because I want to reuse this code elsewhere, and because it needs a
bit of customization. Here's all it is:

A few things stand out. This role has two required parameters, an array
reference of routes to add to the composing controller and the name of the
controller. The latter will be obvious shortly.

Each route corresponds to a single template file which itself has a
page attribute. I do this so that each subdirectory can have its
own layout. (You can accomplish this in many ways with Template Toolkit, but
this approach works best for me right now.)

This action doesn't use Catalyst's own path handling to handle any
arguments, so it effectively takes as many path components as you can provide.
If there's no path component beyond the name of the route, this request
defaults to the index page. (The redirection is a temporary
measure because the system has a few links to static pages such as
/about/jobs.html we're changing to /about/jobs.)

The only remaining interesting part of the code is the call to the
controller's config() method. This is the reason for the
for parameter to this role. Because these controller methods come
from the role, and because the actual body of the method is a closure, Catalyst
can't easily process the normal function attributes you normally use to select
special behaviors. I want to define these methods like:

sub about :Local { ... }
sub contact :Local { ... }

... and so the alternate approach is to set configuration parameters for
each method. That's all the final line of code in the role application block
does.

The best part is that if I want to add a new subdirectory, mapping it in
Catalyst means adding a single entry to this list. Better yet, if I want to add
features to these subdirectories (and I do, per How
Would You Track User Behavior with Plack and Catalyst?, for the purpose of
cohort logging), I can add it in one place.

All of this demonstrates one of my favorite features of Modern Perl:
well-designed abstractions are available when they're necessary. (One of my other favorite features of Modern Perl is that someone's probably already done this, and I just have to find the right plugin on the CPAN to make it happen.)