Now that we have created the skeleton PSGI application we should further improve it. It is clear that we should not have HTML embedded in the Perl code. We need to use some kind of templating system and move all the HTML there. There are tons of templating systems on CPAN, but Template-Toolkit is probably the most well known and most commonly used. It also has plenty of plugins. We are going to us that.

In lib/MetaCPAN/SCO.pm we replace the

return [ '200', [ 'Content-Type' => 'text/plain' ], ['Hello'], ];

with

return template('index');

and then implement the template function. The value passed to the template() function is the name of the template file we need to use to serve this page.

The templates themselves will be located in the tt/ subdirectory in the root of the project. They all have .tt extension. Thus the template of the main page of the site / will called tt/index.tt

We create the tt/ directory and the tt/index.tt file with some dummy content:

MetaCPAN::SCO

This is the content of the page and it is plain text. To make the page really HTML we need to add some HTML-tags before and after. Every web page in a web project have some common attributes. If nothing else then the html tag at the beginning of each file and the closing html tag at the end. Usually though a lot more parts are common. In order to avoid repeating the same HTML code we can put these common elements in separate files and then include them in the main file. Even better Template::Toolkit understand that the beginning and the ending of each page is similar and allows us to configure templates to be processed before and after the main template of each page. Hence we also create a two more files:

tt/incl/header.tt contains

<html>
<head>
  <title>The CPAN Search Site - search.cpan.org</title>
</head>
<body>

tt/incl/footer.tt contains

</body>
</html>
</body>
</html>

Not much, but we have to start somewhere and eliminating the repetition of these parts is already a win. Especially when later we'll want to make some changes. These file were also created in a subdirectory of tt/ called tt/incl/ to have some separation among template that are for pages and templates that are to be included. It is not required, but it makes it easier to understand what is what.

Now let's see the template() function that ties these files together:

We'll have to supply the path to the directory of the templates. At this point I did not have better idea so I looked at the content of the __FILE pseudo variable. It holds the name of the current file. (lib/MetaCPAN/SCO.pm in our case.) abs_path imported from Cwd will convert the relative path of lib/MetaCPAN/SCO.pm to a full path. Something like this: /home/gabor/work/MetaCPAN-SCO/lib/MetaCPAN/SCO.pm. Then the dirname function of File::Basename will return the path withut the last part. Calling it 3 times will return , the root of our project. (Not being able to pass 3 as a second parameter of dirname always bothered me.)

The constructor of Template, the main module of Template::Toolkit, accepts a number of parameters. INCLUDE_PATH is the path to the directory where it is going to look for templates. We just pass to it a full path to the tt/ directory of our project.

We set INTERPOLATE to 0, to avoid expanding strings that look like perl scalars (e.g. $var).

POST_CHOMP will clean up whitespaces.

EVAL_PERL means TT will evaluate Perl code-blocks. I think I usually turn this off. I think I left it on by mistake.

A template is HTML with some embedded special tags defined by Template::Toolkit. By default these tags start with [% and end with %]. The START_TAG and END_TAG directives allow us to change them. The tags I selected will make the whole code look more like plain HTML.

Setting the PRE_PROCESS and POST_PROCESS variables tell Template Toolkit to load the respective template before and after the main template which is going to be passed to the process method. Here we pass the path to the header.tt and footer.tt files relative to the directory provided in the INCLUDE_PATH parameter.

$tt will hold the Template object.

We call the process method of this object with 3 parameters. The first one is the name of the template file. It is the value our template() function received with the .tt extension. The second parameter is a hash reference where later we'll pass the values to be inserted into the template. The 3rd parameter is a reference to an empty scalar variable. The result of the processing will be placed in this variable. As I did not have better idea, I called die in case the process method returned failure. In a web context this might not be a good idea. We might be better off sending back some lame excuse, or a very cryptic error message Internal error 307 to the user and log the failure. For now, during development it will work well.

Once we have the generated HTML in the $out we can return the 3-element array expected by PSGI. The content-Type now really should be text/html and the 3rd value is an array reference with a single element which is the HTMl we have just generated.

sub template {
    my ( $file ) = @_;

    my $root = dirname(dirname(dirname( abs_path(__FILE__) )));

    my $tt = Template->new(
        INCLUDE_PATH => "$root/tt",
        INTERPOLATE  => 0,
        POST_CHOMP   => 1,
        EVAL_PERL    => 1,
        START_TAG    => '<%',
        END_TAG      => '%>',
        PRE_PROCESS  => 'incl/header.tt',
        POST_PROCESS => 'incl/footer.tt',
    );
    my $out;
    $tt->process( "$file.tt", {}, \$out )
        || die $tt->error();
    return [ '200', [ 'Content-Type' => 'text/html' ], [$out], ];
}

In order this to work we also had to load a couple of modules:

use Cwd qw(abs_path);
use File::Basename qw(dirname);
use Plack::Request;
use Template;

These module were also added as prerequisites to Makefile.PL.

Once we have all this we can launch the application again using plackup and visit http://localhost:5000/ to check the result. Don't forget to "view-source" in your browser to see that we really got the HTML.

$ git add .
$ git commit -m "Start using Template toolkit to show the empty pages"

commit