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?
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.