#!/usr/bin/env perl

package main;

use FindBin;
use Cwd;
use lib "$FindBin::Bin/lib";
use Mojolicious::Lite;
use Mojo::Util qw(md5_sum spurt);
use Mojo::Loader;
use Bootylicious::Timestamp;

app->home->parse($ENV{BOOTYLICIOUS_HOME} || getcwd());

push @{app->plugins->namespaces}, 'Bootylicious::Plugin';

plugin 'booty_config' => {file => app->home->rel_file('bootylicious.conf')};
plugin 'markdown_parser';
plugin 'model';

my $ALIAS_RE = qr/[a-zA-Z0-9-_]+/;
my $TAG_RE   = qr/[\w\s]+/;

our $VERSION = '1.01';

if (@ARGV && $ARGV[0] eq '--create-config') {
    if (-e 'bootylicious.conf') {
        die "bootylicious.conf already exists\n";
    }
    
    spurt(Mojo::Loader->data(__PACKAGE__, 'bootylicious.conf'), 'bootylicious.conf');
    exit;
}

get '/'      => \&index => 'root';
get '/index' => \&index => 'index';

sub index {
    my $self = shift;

    my $timestamp = $self->param('timestamp');

    my $pager = $self->get_articles(timestamp => $timestamp);

    $self->stash(articles => $pager->articles, pager => $pager);

    $self->render_smart('index');
}

get '/articles/:year/:month' => [year => qr/\d+/, month => qr/\d+/] =>
  {year => undef, month => undef} => sub {
    my $self = shift;

    my $year    = $self->stash('year');
    my $month   = $self->stash('month');
    my $archive = $self->get_archive(year => $year, month => $month);

    $self->stash(archive => $archive);

    $self->render_smart;
  } => 'articles';

get '/articles/:year/:month/:alias' =>
  [year => qr/\d+/, month => qr/\d+/, alias => $ALIAS_RE] => sub {
    my $self = shift;

    my $article = $self->get_article(@{$self->stash}{qw/year month alias/});

    return $self->render_not_found unless $article;

    $self->stash(article => $article);

    $self->render_smart;
  } => 'article';

post '/articles/:year/:month/:alias/comment' => sub {
    my $self = shift;

    return $self->render_not_found unless $self->comments_enabled;

    my $article = $self->get_article(@{$self->stash}{qw/year month alias/});

    return $self->render_not_found unless $article && $article->comments_enabled;

    my $validator = $self->create_validator;
    my $comment_name = md5_sum($article->created->year, $article->created->month, $article->name);
    
    $validator->field('author')->required(1);
    $validator->field('email')->email(1);
    $validator->field('content')->length(0); # bot protection
    $validator->field($comment_name)->required(1);

    return $self->render('article', article => $article)
      unless $self->validate($validator);
    
    my $comment_params = $validator->values;
    $comment_params->{content} = delete $comment_params->{$comment_name};
    my $comment = $article->comment(%$comment_params);

    return $self->redirect_to($self->href_to_article($article)
          ->fragment('comment-' . $comment->number));
} => 'comment';

get '/comments' => sub {
    my $self = shift;

    return $self->render_not_found unless $self->comments_enabled;

    $self->render_smart;
} => 'comments';

get '/tags/:tag' => [tag => $TAG_RE] => sub {
    my $self = shift;

    my $tag = $self->stash('tag');

    my $timestamp = $self->param('timestamp');

    my $pager = $self->get_articles_by_tag($tag, timestamp => $timestamp);

    return $self->render_not_found unless $pager->articles->size;

    $self->stash(articles => $pager->articles, pager => $pager);

    $self->render_smart;
} => 'tag';

get '/tags' => sub {
    my $self = shift;

    my $cloud = $self->get_tag_cloud;

    $self->stash(tags => $cloud);

    $self->render_smart;
} => 'tags';

get '/pages/:name' => [name => $ALIAS_RE] => sub {
    my $self = shift;

    my $name = $self->stash('name');

    my $page = $self->get_page($name);

    return $self->render_not_found unless $page;

    $self->stash(page => $page);

    $self->render_smart;
} => 'page';

get '/drafts/:name' => [name => $ALIAS_RE] => sub {
    my $self = shift;

    my $name = $self->stash('name');

    my $draft = $self->get_draft($name);

    return $self->render_not_found unless $draft;

    $self->stash(draft => $draft);

    $self->render_smart;
} => 'draft';

app->start;

__DATA__

@@ index.html.ep
% stash description => config('descr');
% if ($articles->size == 0) {
    <div class="text center">
        Nothing here yet :(
    </div>
% }
% while (my $article = $articles->next) {
    %= include 'index-item', article => $article;
% }
%= include 'index-pager', pager => $pager;


@@ index-item.html.ep
    <div class="text">
        <h1 class="title">
            <%= link_to_article $article %>
        </h1>
        <%= include 'article-meta', article => $article %>
        <div class="article-content">
            <%= render_article_or_preview $article %>
        </div>
        % if (comments_enabled) {
        <div class="comment-counter">
            <%= link_to_comments $article %>
        </div>
        % }
    </div>


@@ index-pager.html.ep
    <div id="pager">
        <%= link_to_page 'index', $pager->prev_timestamp => begin %><span class="arrow">&larr; </span><%= strings 'later' %><% end %>
        <%= link_to_page 'index', $pager->next_timestamp => begin %><%= strings 'earlier' %><span class="arrow"> &rarr;</span><% end %>
    </div>


@@ articles.html.ep
% stash title => strings('archive'), description => strings('archive-description');

<div class="text">
    <h1><%= strings 'archive' %></h1>
    <br />
    % if ($archive->is_yearly) {
        %= include 'archive-yearly', archive => $archive;
    % }
    % else {
        %= include 'archive-monthly', articles => $archive->articles;
    % }
</div>

@@ archive-yearly.html.ep
    % while (my $year = $archive->next) {
    <h2><%= $year->year %></h2>
    <ul>
        % while (my $article = $year->articles->next) {
        <li>
            <%= link_to_article $article %>
            <br />
            <%= include 'article-meta', article => $article %>
        </li>
        % }
    </ul>
    % }

@@ archive-monthly.html.ep
    % while (my $article = $articles->next) {
    <li>
        <%= link_to_article $article %>
        <br />
        <%= include 'article-meta', article => $article; %>
    </li>
    % }


@@ article-meta.html.ep
    <div class="article-meta">
        <%= date $article->created %> by <%= article_author $article %>
        <div class="tags"><%= tags_links $article %></div>
    </div>


@@ index.rss.ep
    <channel>
        <title><%= config 'title' %></title>
        <link><%= href_to_rss->to_abs %></link>
        <description><%= config 'description' %></description>
        % my $first = $pager->articles->first;
        % my $first_created = $first ? $first->created
        %   : Bootylicious::Timestamp->new(epoch => 0);
        <pubDate><%= date_rss $first_created %></pubDate>
        <generator><%= generator %></generator>
        % while (my $article = $pager->articles->next) {
        <item>
          <title><%= $article->title %></title>
          <link><%= href_to_article($article)->to_abs %></link>
          <description><![CDATA[
            <%= render_article_or_preview $article %>
            % if ($article->link) {
                <%= permalink_to($article->link) if $article->link %>
            % }
          ]]></description>
          % foreach my $tag (@{$article->tags}) {
          <category><%= $tag %></category>
          % }
          % if (comments_enabled) {
          <comments><%= href_to_comments($article)->to_abs %></comments>
          % }
          <pubDate><%= date_rss $article->created %></pubDate>
          <guid><%= href_to_article($article)->to_abs %></guid>
        </item>
        % }
    </channel>


@@ comments.rss.ep
    % my $comments = get_recent_comments(10);
    <channel>
        <title><%= config 'title' %></title>
        <link><%= href_to_comments_rss->to_abs %></link>
        <description><%= config 'description' %></description>
        % my $first = $comments->first;
        % my $first_created = $first ? $first->created
        %   : Bootylicious::Timestamp->new(epoch => 0);
        <pubDate><%= date_rss $first_created %></pubDate>
        <generator><%= generator %></generator>
        % while (my $comment = $comments->next) {
        <item>
          <title><%= $comment->author %> on <%= $comment->article->title %></title>
          <link><%= href_to_comment($comment)->to_abs %></link>
          <description><![CDATA[
            <%== render_comment $comment %>
          ]]></description>
          <pubDate><%= date_rss $comment->created %></pubDate>
          <guid><%= href_to_comment($comment)->to_abs %></guid>
        </item>
        % }
    </channel>


@@ layouts/wrapper.rss.ep
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xml:base="<%= url_for('index')->to_abs %>"
    xmlns:dc="http://purl.org/dc/elements/1.1/">
    <%= content %>
</rss>


@@ tags.html.ep
% stash title => strings('tags'), description => strings('tags-description');
<div class="text">
    <h1><%= strings 'tags' %></h1>
    <br />
    <div class="tags">
% while (my $tag = $tags->next) {
        <%= link_to_tag $tag %>
        <sub>(<%= $tag->count %>)</sub>
% }
    </div>
</div>


@@ tag.html.ep
% stash title => $tag, description => strings('tag-description', $tag);
<div class="text">
<h1><%= strings 'tag' %> <%= $tag %>
<sup>
<%= link_to_tag $tag => { format => 'rss'} => begin %><img src="/rss.png" alt="RSS" /><% end %></sup>
</h1>
<br />
% while (my $article = $articles->next) {
    <%= link_to_article $article %>
    <br />
    %= include 'article-meta', article => $article;
% }
</div>
%= include 'tag-pager', pager => $pager, tag => $tag;


@@ tag-pager.html.ep
    <div id="pager">
        <%= link_to_page 'tag' => {tag => $tag} => $pager->prev_timestamp => begin %><span class="arrow">&larr; </span><%= strings 'later' %><% end %>
        <%= link_to_page 'tag' => {tag => $tag} => $pager->next_timestamp => begin %><%= strings 'earlier' %><span class="arrow"> &rarr;</span><% end %>
    </div>


@@ tag.rss.ep
%= include 'index', format => 'rss';


@@ article.html.ep
% stash title => $article->title, description => $article->description;
<div class="text">
    <h1 class="title"><%= link_to_article $article %></h1>
    <%= include 'article-meta', article => $article %>
    <div class="article-content">
        <%= render_article $article %>
    </div>
%= include 'article-pingbacks', pingbacks => $article->pingbacks if $article->pingbacks->size;
%= include 'article-comments', comments => $article->comments if comments_enabled && $article->comments->size;
%= include 'article-comment-form' if $article->comments_enabled;
%= include 'article-pager', next => $article->next, prev => $article->prev;
</div>


@@ article-pingbacks.html.ep
<div id="pingbacks">
    <h2>Pingbacks</h2>
    <div class="content">
        <ul>
        % while (my $pingback = $pingbacks->next) {
            <li><%= date $pingback->created %> <%= link_to $pingback->source_uri %></li>
        % }
        </ul>
    </div>
</div>


@@ article-comments.html.ep
<div id="comments">
    <h2>Comments (<%= $comments->size %>)</h2>
    <div class="content">
        % while (my $comment = $comments->next) {
        <div id="comment-<%= $comment->number %>" class="comment">
            <%= gravatar $comment->email %>

            <span class="author"><%= comment_author $comment %></span> says:
            <div class="meta">
                <%= date $comment->created %>
            </div>

            <div class="content">
            <%== render_comment $comment %>
            </div>
        </div>
        % }
    </div>
</div>


@@ article-comment-form.html.ep
<div id="comment-form">
% if ($article->comments_enabled) {
    <h2>Add comment</h2>
    <div class="content">
        <div class="tip-required">
            Fields marked <span class="required">*</span> are required.<br />
            <div class="tip-required-bot">
            This form has a bot protection mechanism, that requires Cookies.<br />
            Please, don't disable them.
            </div>
        </div>
        %= form_for 'comment' => {year => $article->created->year, month => $article->created->month, alias => $article->name}, method => 'post' => begin
        <label for="author">Name <span class="required">*</span></label><br />
        <%= input_tag 'author', class => 'comment' %><br />
        <%= validator_error 'author' %>

        <label for="email">E-mail</label><br />
        <%= input_tag 'email', class => 'comment' %>
        <span class="tip"><%= link_to 'http://gravatar.com' => begin %>Gravatar<% end %>-friendly</span>
        <br />
        <%= validator_error 'email' %>

        <label for="url">Website</label><br />
        <%= input_tag 'url', class => 'comment' %><br />
        <%= validator_error 'url' %>
        
        % use Mojo::Util 'md5_sum';
        % my $comment_name = md5_sum($article->created->year, $article->created->month, $article->name);
        <label for="content">Comment <span class="required">*</span></label><br />
        <%= text_area 'content', class => 'bpr' %>
        <%= text_area $comment_name %><br />
        <%= validator_error $comment_name %>

        <div class="comment-tags">Paragraphs are created automatically. Available tags: [quote], [code].</div>

        <%= submit_button 'Post comment' %>
        % end
    </div>
% }
% else {
    <h2>Comments for this article has been disabled</h2>
% }
</div>


@@ article-pager.html.ep
    <div id="pager">
    <span class="active">
%   if ($prev) {
        <span class="arrow">&larr; </span><%= link_to_article $prev %> &nbsp;
%   }
|
%   if ($next) {
        &nbsp;<%= link_to_article $next %><span class="arrow"> &rarr;</span>
%   }
    </span>
    </div>


@@ page.html.ep
% stash title => $page->title, description => $page->description;
<div class="text">
    <h1 class="title">
        <%= $page->title %>
    </h1>
    <%== render_page $page %>
</div>


@@ draft.html.ep
% stash title => $draft->{title}, description => strings('draft');
<div class="text">
    <h1 class="title">
        <%= $draft->title %>
    </h1>
    <div class="tags"><%= tags_links $draft %></div>
    <%== render_article $draft %>
</div>


@@ not_found.html.ep
% stash title => 'Not found', description => 'Not found';
<div class="error">
    <h1>404</h1>
    <br />
    <%= strings 'not-found' %>
</div>


@@ exception.html.ep
% stash title => 'Error', description => 'Error';
<div class="error">
    <h1>500</h1>
    <br />
    <%= strings 'error' %>
</div>


@@ layouts/wrapper.html.ep
<!doctype html>
    <head>
        <meta charset="UTF-8">
        <title><%= $title ? "$title / " : '' %><%= config 'title' %></title>
        <link rel="stylesheet" href="/styles.css" type="text/css" />
        <%= stylesheet '/styles.css' %>
        <link rel="alternate" type="application/rss+xml" title="<%= config 'title' %>" href="<%= href_to_rss %>" />
        % if (comments_enabled) {
        <link rel="alternate" type="application/rss+xml" title="<%= config 'title' %> Comments" href="<%= href_to_comments_rss %>" />
        % }
        <meta name="generator" content="<%= generator %>" />
        <%= meta %>
        <%= js %>
        <%= css %>
    </head>
    <body>
        <div id="body">
            <div id="header">
                <h1 id="title">
                    <%= link_to_home %>
                    <sup><a href="<%= href_to_rss %>"><img src="/rss.png" alt="RSS" /></a></sup>
                </h1>
                <h2 id="description"><%= config 'description' %></h2>
                % if (my $author = config 'author') {
                <span id="author"><%= config 'author' %></span>,
                % }
                <span id="about"><%= config 'about' %></span>
                <div id="menu"><%= menu %></div>
            </div>
            <div id="content">
            <%= content %>
            </div>
            <div class="push"></div>
        </div>
        <div id="footer"><%== config 'footer' %></div>
    </body>
</html>


@@ styles.css
html, body {height: 100%;margin:0}
body {background: #fff;font-family: Georgia, "Bitstream Charter", serif;line-height:25px}
h1,h2,h3,h4,h5 {font-family: times, "Times New Roman", times-roman, serif; line-height: 40px; letter-spacing: -1px; color: #444; margin: 0 0 0 0; padding: 0 0 0 0; font-weight: 100;}
a,a:active {color:#555}
a:hover{color:#000}
a:visited{color:#000}
img{border:0px}
pre{line-height:18px;border:2px solid #ccc;background:#eee;padding:1em;overflow:auto;overflow-y:visible;width:600px;}
blockquote{border:2px solid #ccc;background:#eee;padding:1em}
#body {width:65%;min-height:100%;height:auto !important;height:100%;margin:0 auto -6em;}
#header {text-align:center;padding:2em 0em 0.5em 0em;border-bottom: 1px solid #000}
h1#title{font-size:3em}
h2#description{font-size:1.5em;color:#999}
span#author {font-weight:bold}
span#about {font-style:italic}
#menu {padding-top:1em;text-align:right}
#content {background:#FFFFFF}
.article-meta {line-height:18px;color:#999;margin-left:10px;font-size:small;font-style:italic;padding-bottom:0.5em}
.artcle-content {}
.modified {margin:0px}
.tags a{color:#999}
.text {padding:2em;}
.text h1.title {font-size:2.5em}
.error {padding:2em;text-align:center}
.more {margin-left:10px;padding-bottom:1em;}
#pager {text-align:center;padding:2em; color:#ccc}}
#pager span.active {color:#000}
#pager span.arrow {background:#fff}
#subfooter {padding:2em;border-top:#000000 1px solid}
#footer{width:65%;margin:auto;font-size:80%;text-align:center;padding:2em 0em 2em 0em;border-top:#000000 1px solid;height:2em;}
.center {text-align:center}
.push {height:6em}
input.comment, textarea {font-size:150%;width:60%}
textarea {height:200px}
textarea.bpr {display: none}
label {color:#999}
.required {color:red}
#pingbacks, #comments, #comment-form {padding:1em 0px}
#pingbacks .content, #comments .content, #comment-form .content {padding-left:1em}
#comments .comment .meta {line-height:18px;color:#999;margin-left:10px;font-size:small;font-style:italic;padding-bottom:0.5em}
#comments .comment .author {font-weight:bold}
#comments .comment .content {padding-bottom:2em}
.tip {font-size:smaller;color:#bbb;font-style:italic;padding-left:1em}
.tip a{color:#bbb}
.tip-required {padding:1em 0px;}
.tip-required-bot {font-style:italic;font-size:small;color:#999;line-height:18px}
.comment-tags {font-style:italic;font-size:small;color:#999;line-height:18px;margin-bottom:1em}
.content div.error {text-align:left;padding:0;color:red}
img.gravatar {float:left; padding-right:0.5em}
.comment-counter {color:#999;font-size:small;margin-left:10px}
.comment-counter a {color:#999}


@@ rss.png (base64)
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
bWFnZVJlYWR5ccllPAAAAlJJREFUeNqkU0toU0EUPfPJtOZDm9gSPzWVKloXgiCCInXTRTZVQcSN
LtyF6qILFwoVV+7EjR9oFy7VlSAVF+ouqMWWqCCIrbYSosaARNGmSV7ee+OdyUsMogtx4HBn5t1z
7twz85jWGv8zZHaUmRjlHBnBkRYSCSnog/wzuECZMzxgDNPEW5E0ASHTl4qf6h+KD6iwUpwyuRCw
kcCCNSPoRsNZKeS31D8WTOHLkqoagbQhV+sV1fDqEJQoidSCCMiMjskZU9HU4AAJpJsC0gokTGVD
XnfhA0DRL7+Hn38M/foOeOUzOJEZs+2Cqy5F1iXs3PZLYEGl+ux1NF7eAmpfIXedQOjYbYgdh9tk
Y3oTsDAnNCewPZqF8/SKjdqs+7aCj5wFDkwSlUEvzFgyPK8twNvuBv3GzixgzfgcQmNXqW/68IgE
is+BvRPQ0fXE9eC7Lvy/Cfi5G8DSQ7DkTrCxKbrgJPSTS5TUDQwfgWvIBO0Dvv+bgPFAz12Dzl4E
7p5svpQ9p6HLy9DFF2CD+9sCHpG9DgHHeGAExDglZnLAj09APgts2N089pdFsPjmXwIuHAJk8JKL
rXtuDWtWtQwWiliScFapQJedKxKsVFA0KezVUeMvprcfHDkua6uRzqsylQ2hE2ZPqXAld+/tTfIg
I56VgNG1SDkuhmIb+3tELCLRTYYpRdVDFpwgCJL2fJfXFufLS4Xl6v3z7zBvXkdqUxjJc8M4tC2C
fdDoNe62XPaCaOEBVOjbm++YnSphpuSiZAR6CFQS4h//ZJJD7acAAwCdOg/D5ZiZiQAAAABJRU5E
rkJggg==

@@ bootylicious.conf
%# Bootylicious configuration
%#
%# This configuration is in JSON format, that is preprocessed by Mojo::Template.
%# Yes, you can use Perl here.

{
%#  Change this to something not guessable
%#    "secret" : "Unique string",

%#  Blog description settings
%#    "author"       : "whoami",
%#    "email"        : "",
%#    "title"        : "Just another blog",
%#    "about"        : "Perl hacker",
%#    "description"  : "I do not know if I need this",

%#  Article rendering settings
%#    "cuttag"    : "[cut]",
%#    "cuttext"   : "Keep reading",
%#    "pagelimit" : 10,
%#    "datefmt"   : "%a, %d %b %Y"

%#  Appearence settings
%#    "menu" : [
%#        "index",   "/",
%#        "tags",    "/tags.html",
%#        "archive", "/articles.html"
%#    ],
%#    "footer" : "Powered by <a href=\"https://github.com/vti/bootylicious\">Bootylicious</a>",
%#    "theme"     : "",

%#  Additional HTML tags
%#    "meta"      : [],
%#    "css"       : [],
%#    "js"        : [],

%#  Plugins
%#    "plugins" : [
%#        "admin", {"username" : "foo", "password" : "bar"}
%#        "search",
%#        "google_analytics", {"urchin" : "UA-12345"},
%#        "pingback"
%#    ],

%#  Enabling/disabling comments globbaly
%#    "comments_enabled" : true,

%#  System settings
%#    "perl5lib" : "",
%#    "loglevel" : "error",

%#  Path settings
%#    "articles_directory"  : "articles",
%#    "pages_directory"     : "pages",
%#    "drafts_directory"    : "drafts",
%#    "public_directory"    : "public",
%#    "templates_directory" : "templates",

%#  Don't worry about trailing comma
    "ok" : true
}

__END__

=head1 NAME

Bootylicious - Lightweight blog engine on Mojo steroids!

=head1 DESCRIPTION

Bootylicious is a minimalistic blogging application built on top of
L<Mojolicious::Lite>. It is easily extendable with plugins, templates and
themes.

=head2 Features

=over

    * filesystem-based storage
    * comments
    * tags
    * RSS (articles, comments and by tag)
    * static pages
    * drafts
    * archive
    * pingbacks
    * themes
    * multi-parser support (POD, Markdown)
    * plugins
    * Unicode support
    * search

    * lightweight
    * clean code
    * runs everywhere

=back

=head2 Setup

    $ cpan Bootylicious
    $ bootylicious daemon
    Server available at http://*:3000.

=head2 Configuration

Create default configuration file

    $ bootylicious --create-config

Then open C<bootylicious.conf> and change it to fit your
needs. By default C<bootylicious> uses current directory
as working directory. You can change this with C<BOOTYLICIOUS_HOME>
environment variable.

=head2 Writing articles

Articles by default go into C<articles> directory.

Article consists of file information and content with meta data.

=head3 File info

    20101017-article.pod

    or

    20101017T14:02:00-article.pod

Where timestamp tells us when the article was created. Modified time is
retrieved automatically from C<mtime>. Filename is the article's permalink url.
Extension is article's format.

=head3 Content

    Title: My first article
    Tags: blog, internet

    Welcome!

    [cut] Read more

    This is my first article. It is in C<pod> format. And I can use all kind of
    B<tags>.

Every article should have metadata. Metadata ends with an empty line.  If there
is a C<[cut]> tag, article will be splitted into C<preview> and C<content>
parts. C<preview> is shown when a) article list is requested, b) rss.

Depending on file format (file extention, remember?) the content is parsed
with an appropriate parser. POD format is available by default. Markdown format
is available when L<Text::Markdown> is installed. Other formats are available as
plugins.

=head2 Enabling/disabling comments

Comments can be disabled everywhere by setting C<comments_enabled> to C<false> in
configuration file:

    {
        ...
        "comments_enabled" : false,
        ...
    }

Or comments can be disabled on per article:

    Title: Article with no comments allowed
    Comments: false

    This is an article...

=head2 Core plugins

=head3 Bootylicious::Plugin::Pingback

Pingbacks as described on L<http://www.hixie.ch/specs/pingback/pingback>.

=head3 Bootylicious::Plugin::HttpCache

ETag header settings and checks.

=head3 Bootylicious::Plugin::CanonicalUrl

All urls that don't have C<.html> in their paths are redirected to C<.html>.

=head3 Bootylicious::Plugin::GoogleAnalytics

Google Analytics JavaScript code.

=head3 Bootylicious::Plugin::Search

Basic search.
