Generating a static website from templates with Perl

For one of my websites I wanted to create static HTML but keep headers, footers and other tokens the same between pages.

For a few pages "copy and paste" was all I needed, but as the site grew it became a time consuming process to change every page each time I wanted to changed some links in the header.

One option is to use Ant's Filterset. This is an excellent solution, especially for managing environmentally specific property files.

I wanted more control (specifically I wanted to ignore subversion .svn directories), so I ended up writing my own solution in Perl.

Creating static output for a template based website using Perl

Notes

To reference a template, us the uppercase name of the HTML file in the templates directory as the variable name. For example <TMPL_VAR NAME=FOOBAR> refers to templates/foobar.html

The code ignores subversion files. i.e. anything in .svn is not copied over to target.

Only templates ending in ".html" are used. For source files, the code checks to see if the file is text or binary. Only text files are merged. Binary files are copied over to target unchanged.

If the target directory already exists, the code renames it to target_old. If target_old exists, the code errors and stops, so remove the target_old directory if it exists before running build.pl

The code will work with multiple levels of directories underneath "source"

Your file structure should look like this:
Working_directory/

build.pl - the perl script below
templates/ - directory containing HTML fragments, such as foobar.html
source/ - directory containing HTML pages such as index.html

The Perl Code

#!/usr/bin/perl -w
use strict;
use HTML::Template; # Comes with standard build
use File::Find;
use File::Copy;

my %templates;
my $templateDir = "templates";
my $sourceDir = "source";
my $destDir = "target";

sub main {

    # Read in the templates
    find(\&processTemplate, $templateDir);
    
    # Create target directory
    die "Please remove target_old directory first" if ( -d $destDir.'_old'); 
    if (-d $destDir ) {
        rename($destDir ,$destDir.'_old') or die "Failed to rename $destDir to ${destDir}_old\n";
    }

    # Go through source
    find({wanted => \&processSourceContext, no_chdir => 1 } , $sourceDir );

    print "Completed.\n";
}


sub readFile {
    # $_ is path/filename
    # Returns file contents
    open FILE, "<$_";
    my $fileContents = do { local $/; <FILE> };
    close FILE;
    return $fileContents;
}

sub processTemplate {     
    my $basename = $_;     # basename
    my $filename = $File::Find::name; # Full path

    if ($filename =~ /.html$/ && ! ( $filename =~ /\.svn/ ) ) {
        # Name of this template will be the file basename withouth the extension
        my $templateName = $basename;
        $templateName =~ s/\.[\w\d]+$//; 
        $templateName = uc($templateName);

        print "Processing template $templateName\n";        
        $templates{$templateName} = readFile($filename);
    }
} 
sub processFile {
    # $_ is path/file
    
    # open the html source
    my $template = HTML::Template->new(filename => $_, die_on_bad_params => 0);

    # Process the templates
    while ((my $key, my $value) = each(%templates)){
        print "Using template $key\n";
        $template->param($key => $value);    
    }
    $_ = sourceToTarget($_);

    open (FILE,">$_") or die "I cannot open $_ to write.\n";
    print FILE $template->output;
    close(FILE);

}
sub processSourceContext {
    # $_ is path/file

    if ( /\.svn/ || /^\.+$/ ) {
        print "Ignoring $_\n";
    } elsif ( -d ) {
        # Create the same directory in the target
        print "Creating directory $_\n";
        mkdir(sourceToTarget($_)) or die $!;
    } elsif ( -T ) { 
        # Text File
        print "Processing $_\n";
        processFile($_);
    } elsif ( -B ) {
        # Binary file
        print "Copying binary file $_\n";
        my $source=$_;
        copy($source, sourceToTarget($source)) or die "Copy failed: $!";
    } else {
        # Anything else...
        die "Encountered $_ and I do not know what to do with it";
    }
    
}
sub sourceToTarget {
    # Returns target directory/file structure, given source directory/file structure
    s/$sourceDir/$destDir/;
    return $_;
}
main();