Search code examples
perlmojolicious

Can I use Mojolicious to build a static site?


Is it possible to use the Mojolicious template system to build a static website?

I'm trying to use a (skeleton) script like this:

use Mojo::Template;
use Mojolicious::Plugin::DefaultHelpers;
use Mojolicious::Plugin::TagHelpers;

my $mt = Mojo::Template->new;
print $mt->render_file('index.html.ep');

Where index.html.ep is like this:

% layout 'default';
This is a foo

However I get an error nessage:

String found where operator expected at index.html.ep line 1, near "layout 'default'"
    (Do you need to predeclare layout?)
syntax error at index.html.ep line 1, near "layout 'default'"
1: % layout 'default';
2: This is a foo

Obviously, if I omit % layout 'default'; all is good, but being able to reuse snippets and layout is the whole point.

I know I could use Template Toolkit or some other templating system, but I want to avoid the cognitive friction of using multiple systems if at possible.

I'm also aware that I could start up mojolicious as a server and wget all the pages, but it seems overkill.

Any help here?


Solution

  • You can use Mojo templates outside of the Mojolicious web framework – I used to do that to render static pages for my blog. However, the Mojo::Template does not come with the normal helpers by default. Instead, the rest of Mojolicous injects variables and helpers into the template.

    For my blog, I decided to implement my own helper system. I'll describe my solution in the rest of this answer. Mojo may have changed in the meantime, and may prefer some different solution.

    I modelled a template as a pair of a stash reference and the Mojo::Template object. Each Template is compiled into its own package. Later, we can inject temporary values into the stash reference and communicate values to the outside. A helper is a closure of a specific stash ref, so it can access these values without using an explicit parameter.

    Here's how templates are compiled:

    package AMON::Blog::TemplateCollection;
    
    sub add_template($self, $name, $source) {
        state $namespace_id = 0;
        my $namespace = Package::Stash->new(
            __PACKAGE__ . '::Namespace::' . ++$namespace_id);
    
        my $template = Mojo::Template->new(
            name => $name,
            namespace => $namespace->name,
            auto_escape => 1,
            tag_start => '{{',
            tag_end => '}}',
        );
    
        # enter the helpers into the namespace
        my $stash_ref = \{};
        while (my ($name, $code) = each %{ $self->helpers }) {
            $namespace->add_symbol('&' . $name => $code->($stash_ref));
        }
    
        $template->parse($source);
    
        $self->templates->{$name} = {
            stash_ref => $stash_ref,
            template => $template
        };
    
        return;
    }
    

    Here is a layout helper that writes the requested layout into a stash variable:

    layout => sub ($stash_ref) {
        return sub ($name, %args) {
            if (my $existing = $$stash_ref->{layout}) {
                croak sprintf q(Can't change layout from "%s" to "%s"), $existing->{name}, $name;
            }
            $$stash_ref->{layout} = { name => $name, args => \%args };
        };
    },
    

    The outer sub is only used to close over the $stash_ref, and is executed during template compilation above.

    To render the template, we supply temporary stash values, then process the Mojo::Template. If the stash contains a layout argument, we recurse to render the layout template with the current template's output as content:

    sub render($self, $name, %args) {
        my $template = $self->templates->{$name}
            // croak qq(Unknown template "$name");
    
        my ($stash_ref, $template_object) = @$template{qw/stash_ref template/};
    
        $$stash_ref = {
            name => $name,
            layout => undef,
            args => \%args,
        };
    
        my $result = $template_object->process();
    
        my $layout_args = $$stash_ref->{layout};
        $$stash_ref = undef;
    
        if (blessed $result and $result->isa('Mojo::Exception')) {
            die $result;
        }
    
        if ($layout_args) {
            my $name = $layout_args->{name};
            my $args = $layout_args->{args};
            return $self->render($name, %$args, content => $result);
        }
    
        return $result;
    }
    

    This approach is not terribly elegant, but it works without having to pull in all the rest of Mojolicious (particularly controllers, which are pointless for a static site). Some time later I switched to a different template engine that supports template inheritance out of the box, without such extensive workarounds.