#!/usr/bin/env perl
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">← </span><%= strings 'later' %><% end %>
<%= link_to_page 'index', $pager->next_timestamp => begin %><%= strings 'earlier' %><span class="arrow"> →</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">← </span><%= strings 'later' %><% end %>
<%= link_to_page 'tag' => {tag => $tag} => $pager->next_timestamp => begin %><%= strings 'earlier' %><span class="arrow"> →</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">← </span><%= link_to_article $prev %>
% }
|
% if ($next) {
<%= link_to_article $next %><span class="arrow"> →</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
}