The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/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 qw(data_section);
use Bootylicious;
use Bootylicious::Timestamp;

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

unshift @{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-]+/;

if (@ARGV && $ARGV[0] eq '--create-config') {
    if (-e 'bootylicious.conf') {
        die "bootylicious.conf already exists\n";
    }
    
    spurt(data_section(__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->reply->not_found unless $article;

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

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

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

    return $self->reply->not_found unless $self->comments_enabled;

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

    return $self->reply->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->reply->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->reply->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->reply->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->reply->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">
        % my $author = article_author $article;
        <%= date $article->created %> <%= $author ? 'by '.$author : '' %>
        <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
% die('RSS disabled') unless config 'rss_enabled';
<?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 %>
% if (config 'rss_enabled') {
<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 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' %>
        % if (config 'rss_enabled') {
        <link rel="alternate" type="application/rss+xml" title="<%= config 'title' %>" href="<%= href_to_rss %>" />
        % }
        % if (comments_enabled && config 'rss_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 %>
                    % if (config 'rss_enabled') {
                    <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,

%#  Enabling/disabling rss
%#    "rss_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
}