File Coverage

blib/lib/Statocles/App/Blog.pm
Criterion Covered Total %
statement 150 159 94.3
branch 31 40 77.5
condition 5 10 50.0
subroutine 16 16 100.0
pod 7 7 100.0
total 209 232 90.0


line stmt bran cond sub pod time code
1             package Statocles::App::Blog;
2             our $VERSION = '0.084';
3             # ABSTRACT: A blog application
4              
5 24     24   51123 use Text::Unidecode;
  24         27113  
  24         1388  
6 24     24   168 use Statocles::Base 'Class';
  24         55  
  24         242  
7 24     24   162935 use Getopt::Long qw( GetOptionsFromArray );
  24         61  
  24         246  
8 24     24   3903 use Statocles::Store;
  24         57  
  24         619  
9 24     24   8699 use Statocles::Page::Document;
  24         88  
  24         833  
10 24     24   10317 use Statocles::Page::List;
  24         186  
  24         987  
11 24     24   196 use Statocles::Util qw( run_editor );
  24         52  
  24         62536  
12              
13             with 'Statocles::App::Role::Store';
14              
15             #pod =attr store
16             #pod
17             #pod # site.yml
18             #pod blog:
19             #pod class: Statocles::App::Blog
20             #pod args:
21             #pod store: _posts
22             #pod
23             #pod The L<store directory path|Statocles::Store> to read for blog posts. Required.
24             #pod
25             #pod The Blog directory is organized in a tree by date, with a directory for the
26             #pod year, month, day, and post. Each blog post is its own directory to allow for
27             #pod additional files for the post, like images or additional pages.
28             #pod
29             #pod =cut
30              
31             #pod =attr tag_text
32             #pod
33             #pod # site.yml
34             #pod blog:
35             #pod class: Statocles::App::Blog
36             #pod args:
37             #pod tag_text:
38             #pod software: Posts about software and development
39             #pod travel: My travelogue around the world!
40             #pod
41             #pod A hash of tag and introductory Markdown that will be shown on the tag's main
42             #pod page. Having a description is optional.
43             #pod
44             #pod Using L<Beam::Wire's $config directive|Beam::Wire/Config Services>, you can
45             #pod put the tag text in an external file:
46             #pod
47             #pod # site.yml
48             #pod blog:
49             #pod class: Statocles::App::Blog
50             #pod args:
51             #pod tag_text:
52             #pod $config: tags.yml
53             #pod
54             #pod # tags.yml
55             #pod software: |-
56             #pod # Software
57             #pod
58             #pod Posts about software development, mostly in [Perl](http://perl.org)
59             #pod
60             #pod travel: |-
61             #pod # Travel
62             #pod
63             #pod My travelogue around the world! [Also visit my Instagram!](http://example.com)
64             #pod
65             #pod =cut
66              
67             has tag_text => (
68             is => 'ro',
69             isa => HashRef,
70             default => sub { {} },
71             );
72              
73             #pod =attr page_size
74             #pod
75             #pod # site.yml
76             #pod blog:
77             #pod class: Statocles::App::Blog
78             #pod args:
79             #pod page_size: 5
80             #pod
81             #pod The number of posts to put in a page (the main page and the tag pages). Defaults
82             #pod to 5.
83             #pod
84             #pod =cut
85              
86             has page_size => (
87             is => 'ro',
88             isa => Int,
89             default => sub { 5 },
90             );
91              
92             #pod =attr index_tags
93             #pod
94             #pod # site.yml
95             #pod blog:
96             #pod class: Statocles::App::Blog
97             #pod args:
98             #pod index_tags: [ '-private', '+important' ]
99             #pod
100             #pod Filter the tags shown in the index page. An array of tags prefixed with either
101             #pod a + or a -. By prefixing the tag with a "-", it will be removed from the index,
102             #pod unless a later tag prefixed with a "+" also matches.
103             #pod
104             #pod By default, all tags are shown on the index page.
105             #pod
106             #pod So, given a document with tags "foo", and "bar":
107             #pod
108             #pod index_tags: [ ] # document will be included
109             #pod index_tags: [ '-foo' ] # document will not be included
110             #pod index_tags: [ '-foo', '+bar' ] # document will be included
111             #pod
112             #pod =cut
113              
114             has index_tags => (
115             is => 'ro',
116             isa => ArrayRef[Str],
117             default => sub { [] },
118             );
119              
120             #pod =attr template_dir
121             #pod
122             #pod The directory (inside the theme directory) to use for this app's templates.
123             #pod Defaults to C<blog>.
124             #pod
125             #pod =cut
126              
127             has '+template_dir' => (
128             default => 'blog',
129             );
130              
131             # A cache of the last set of post pages we have
132             # XXX: We need to allow apps to have a "clear" the way that Store and Theme do
133             has _post_pages => (
134             is => 'rw',
135             isa => ArrayRef,
136             predicate => '_has_cached_post_pages',
137             );
138              
139             # The default post information hash
140             has _default_post => (
141             is => 'rw',
142             isa => HashRef,
143             lazy => 1,
144             default => sub {
145             {
146             tags => undef,
147             content => "Markdown content goes here.\n",
148             }
149             },
150             );
151              
152             #pod =method command
153             #pod
154             #pod my $exitval = $app->command( $app_name, @args );
155             #pod
156             #pod Run a command on this app. The app name is used to build the help, so
157             #pod users get exactly what they need to run.
158             #pod
159             #pod =cut
160              
161             my $USAGE_INFO = <<'ENDHELP';
162             Usage:
163             $name help -- This help file
164             $name post [--date YYYY-MM-DD] <title> -- Create a new blog post with the given title
165             ENDHELP
166              
167             sub command {
168 6     6 1 28256 my ( $self, $name, @argv ) = @_;
169              
170 6 100       23 if ( !$argv[0] ) {
171 1         31 say STDERR "ERROR: Missing command";
172 1         76 say STDERR eval "qq{$USAGE_INFO}";
173 1         8 return 1;
174             }
175              
176 5 100       29 if ( $argv[0] eq 'help' ) {
    100          
177 1         78 say eval "qq{$USAGE_INFO}";
178             }
179             elsif ( $argv[0] eq 'post' ) {
180 3         7 my %opt;
181 3         11 my @doc_opts = qw(author layout status tags template);
182             GetOptionsFromArray( \@argv, \%opt,
183             'date:s',
184 3         10 map { "$_:s" } @doc_opts,
  15         47  
185             );
186              
187             my %doc = (
188 3         73 %{ $self->_default_post },
189 3 50       1759 (map { defined $opt{$_} ? ( $_, $opt{$_} ) : () } @doc_opts),
  15         107  
190             title => join " ", @argv[1..$#argv],
191             );
192              
193             # Read post content on STDIN
194 3 50       26 if ( !-t *STDIN ) {
195 3         9 my $content = do { local $/; <STDIN> };
  3         12  
  3         63  
196 3         39 %doc = (
197             %doc,
198             $self->store->parse_frontmatter( "<STDIN>", $content ),
199             );
200              
201             # Re-open STDIN as the TTY so that the editor (vim) can use it
202             # XXX Is this also a problem on Windows?
203 3 50       31 if ( -e '/dev/tty' ) {
204 3         15 close STDIN;
205 3         51 open STDIN, '/dev/tty';
206             }
207             }
208              
209 3 100 66     26 if ( !$ENV{EDITOR} && !$doc{title} ) {
210 1         42 say STDERR <<"ENDHELP";
211             Title is required when \$EDITOR is not set.
212              
213             Usage: $name post <title>
214             ENDHELP
215 1         8 return 1;
216             }
217              
218 2         7 my ( $year, $mon, $day );
219 2 100       8 if ( $opt{ date } ) {
220 1         5 ( $year, $mon, $day ) = split /-/, $opt{date};
221             }
222             else {
223 1         17 ( undef, undef, undef, $day, $mon, $year ) = localtime;
224 1         5 $year += 1900;
225 1         3 $mon += 1;
226             }
227              
228 2         23 my @date_parts = (
229             sprintf( '%04i', $year ),
230             sprintf( '%02i', $mon ),
231             sprintf( '%02i', $day ),
232             );
233              
234 2   50     14 my $slug = $self->make_slug( $doc{title} || "new post" );
235 2         17 my $path = Path::Tiny->new( @date_parts, $slug, "index.markdown" );
236 2         88 $self->store->write_document( $path => \%doc );
237 2         19 my $full_path = $self->store->path->child( $path );
238              
239 2 50       95 if ( run_editor( $full_path ) ) {
240 0         0 my $old_title = $doc{title};
241 0         0 %doc = %{ $self->store->read_document( $path ) };
  0         0  
242 0 0       0 if ( $doc{title} ne $old_title ) {
243 0         0 $self->store->path->child( $path->parent )->remove_tree;
244 0   0     0 $slug = $self->make_slug( $doc{title} || "new post" );
245 0         0 $path = Path::Tiny->new( @date_parts, $slug, "index.markdown" );
246 0         0 $self->store->write_document( $path => \%doc );
247 0         0 $full_path = $self->store->path->child( $path );
248             }
249             }
250              
251 2         9 say "New post at: $full_path";
252              
253             }
254             else {
255 1         43 say STDERR qq{ERROR: Unknown command "$argv[0]"};
256 1         84 say STDERR eval "qq{$USAGE_INFO}";
257 1         9 return 1;
258             }
259              
260 3         125 return 0;
261             }
262              
263             #pod =method make_slug
264             #pod
265             #pod my $slug = $app->make_slug( $title );
266             #pod
267             #pod Given a post title, remove special characters to create a slug.
268             #pod
269             #pod =cut
270              
271             sub make_slug {
272 2499     2499 1 7030 my ( $self, $slug ) = @_;
273 2499         6662 $slug = unidecode($slug);
274 2499         43819 $slug =~ s/'//g;
275 2499         7987 $slug =~ s/[\W]+/-/g;
276 2499         7823 $slug =~ s/^-|-$//g;
277 2499         13860 return lc $slug;
278             }
279              
280             #pod =method index
281             #pod
282             #pod my @pages = $app->index( \@post_pages );
283             #pod
284             #pod Build the index page (a L<list page|Statocles::Page::List>) and all related
285             #pod feed pages out of the given array reference of post pages.
286             #pod
287             #pod =cut
288              
289             my %FEEDS = (
290             rss => {
291             text => 'RSS',
292             template => 'index.rss',
293             },
294             atom => {
295             text => 'Atom',
296             template => 'index.atom',
297             },
298             );
299              
300             sub index {
301 55     55 1 161 my ( $self, $all_post_pages ) = @_;
302              
303 55         117 my @index_tags;
304             my %tag_flag;
305 55         127 for my $tag_spec ( map lc, @{ $self->index_tags } ) {
  55         312  
306 6         15 my $tag = substr $tag_spec, 1;
307 6         10 push @index_tags, $tag;
308 6         29 $tag_flag{$tag} = substr $tag_spec, 0, 1;
309             }
310              
311 55         118 my @index_post_pages;
312 55         153 PAGE: for my $page ( @$all_post_pages ) {
313 194         320 my $page_flag = '+';
314 194         253 my %page_tags;
315 194         595 @page_tags{ map lc, @{ $page->document->tags } } = 1; # we use exists(), so value doesn't matter
  194         2813  
316 194         1589 for my $tag ( map lc, @index_tags ) {
317 27 100       54 if ( exists $page_tags{ $tag } ) {
318 10         17 $page_flag = $tag_flag{ $tag };
319             }
320             }
321 194 100       596 push @index_post_pages, $page if $page_flag eq '+';
322             }
323              
324 55         477 my @pages = Statocles::Page::List->paginate(
325             after => $self->page_size,
326             path => $self->url_root . '/page/%i/index.html',
327             index => $self->url_root . '/index.html',
328             pages => [ _sort_page_list( @index_post_pages ) ],
329             app => $self,
330             template => $self->template( 'index.html' ),
331             layout => $self->template( 'layout.html' ),
332             );
333              
334 55 100       338 return unless @pages; # Only build feeds if we have pages
335              
336 54         132 my $index = $pages[0];
337 54         125 my @feed_pages;
338             my @feed_links;
339 54         358 for my $feed ( sort keys %FEEDS ) {
340             my $page = Statocles::Page::List->new(
341             app => $self,
342             pages => $index->pages,
343             path => $self->url_root . '/index.' . $feed,
344 108         3778 template => $self->template( $FEEDS{$feed}{template} ),
345             links => {
346             alternate => [
347             $self->link(
348             href => $index->path,
349             title => 'index',
350             type => $index->type,
351             ),
352             ],
353             },
354             );
355              
356 108         4644 push @feed_pages, $page;
357             push @feed_links, $self->link(
358             text => $FEEDS{ $feed }{ text },
359 108         1865 href => $page->path->stringify,
360             type => $page->type,
361             );
362             }
363              
364             # Add the feeds to all the pages
365 54         3267 for my $page ( @pages ) {
366 86         1706 $page->links( feed => @feed_links );
367             }
368              
369 54         372 return ( @pages, @feed_pages );
370             }
371              
372             #pod =method tag_pages
373             #pod
374             #pod my @pages = $app->tag_pages( \%tag_pages );
375             #pod
376             #pod Get L<pages|Statocles::Page> for the tags in the given blog post documents
377             #pod (build from L<the post_pages method|/post_pages>, including relevant feed
378             #pod pages.
379             #pod
380             #pod =cut
381              
382             sub tag_pages {
383 55     55 1 180 my ( $self, $tagged_docs ) = @_;
384              
385 55         109 my @pages;
386 55         481 for my $tag ( keys %$tagged_docs ) {
387             my @tag_pages = Statocles::Page::List->paginate(
388             after => $self->page_size,
389             path => join( "/", $self->url_root, 'tag', $self->_tag_url( $tag ), 'page/%i/index.html' ),
390             index => join( "/", $self->url_root, 'tag', $self->_tag_url( $tag ), 'index.html' ),
391 185         878 pages => [ _sort_page_list( @{ $tagged_docs->{ $tag } } ) ],
392             app => $self,
393             template => $self->template( 'index.html' ),
394             layout => $self->template( 'layout.html' ),
395             data => {
396 185         891 tag_text => $self->tag_text->{ $tag },
397             },
398             );
399              
400 185         599 my $index = $tag_pages[0];
401 185         305 my @feed_pages;
402             my @feed_links;
403 185         831 for my $feed ( sort keys %FEEDS ) {
404 370         11848 my $tag_file = $self->_tag_url( $tag ) . '.' . $feed;
405              
406             my $page = Statocles::Page::List->new(
407             app => $self,
408             pages => $index->pages,
409             path => join( "/", $self->url_root, 'tag', $tag_file ),
410 370         1201 template => $self->template( $FEEDS{$feed}{template} ),
411             links => {
412             alternate => [
413             $self->link(
414             href => $index->path,
415             title => $tag,
416             type => $index->type,
417             ),
418             ],
419             },
420             );
421              
422 370         14818 push @feed_pages, $page;
423             push @feed_links, $self->link(
424             text => $FEEDS{ $feed }{ text },
425 370         6050 href => $page->path->stringify,
426             type => $page->type,
427             );
428             }
429              
430             # Add the feeds to all the pages
431 185         11118 for my $page ( @tag_pages ) {
432 217         4023 $page->links( feed => @feed_links );
433             }
434              
435 185         603 push @pages, @tag_pages, @feed_pages;
436             }
437              
438 55         336 return @pages;
439             }
440              
441             #pod =method pages
442             #pod
443             #pod my @pages = $app->pages( %options );
444             #pod
445             #pod Get all the L<pages|Statocles::Page> for this application. Available options
446             #pod are:
447             #pod
448             #pod =over 4
449             #pod
450             #pod =item date
451             #pod
452             #pod The date to build for. Only posts on or before this date will be built.
453             #pod Defaults to the current date.
454             #pod
455             #pod =back
456             #pod
457             #pod =cut
458              
459             # sub pages
460             around pages => sub {
461             my ( $orig, $self, %opt ) = @_;
462             $opt{date} ||= DateTime::Moonpig->now( time_zone => 'local' )->ymd;
463             my $root = $self->url_root;
464             my $is_dated_path = qr{^$root/?(\d{4})/(\d{2})/(\d{2})/};
465             my @parent_pages = $self->$orig( %opt );
466             my @pages =
467             map { $_->[0] }
468             # Only pages today or before
469             grep { $_->[1] le $opt{date} }
470             # Create the page's date
471             map { [ $_, join "-", $_->path =~ $is_dated_path ] }
472             # Only dated pages
473             grep { $_->path =~ $is_dated_path }
474             #$self->$orig( %opt );
475             @parent_pages;
476             @pages = _sort_page_list( @pages );
477              
478             my @post_pages;
479             my %tag_pages;
480              
481             for my $page ( @pages ) {
482              
483             if ( $page->isa( 'Statocles::Page::Document' ) ) {
484              
485             if ( $page->path =~ m{$is_dated_path [^/]+ (?:/index)? [.]html$}x ) {
486             my ( $year, $month, $day ) = ( $1, $2, $3 );
487              
488             push @post_pages, $page;
489              
490             my $doc = $page->document;
491             $page->date( $doc->has_date ? $doc->date : DateTime::Moonpig->new( year => $year, month => $month, day => $day ) );
492              
493             my @tags;
494             for my $tag ( @{ $doc->tags } ) {
495             push @{ $tag_pages{ lc $tag } }, $page;
496             push @tags, $self->link(
497             text => $tag,
498             href => join( "/", 'tag', $self->_tag_url( $tag ), '' ),
499             );
500             }
501             $page->tags( \@tags );
502              
503             $page->template( $self->template( 'post.html' ) );
504             }
505             }
506             }
507              
508             for ( my $i = 0; $i < @post_pages; $i++ ) {
509              
510             my $page = $post_pages[$i];
511             my $prev_page = $i ? $post_pages[$i-1] : undef;
512             my $next_page = $post_pages[$i+1];
513             $page->prev( $prev_page->path ) if $prev_page;
514             $page->next( $next_page->path ) if $next_page;
515             }
516              
517             # Cache the post pages for this build
518             # XXX: This needs to be handled more intelligently with proper dependencies
519             $self->_post_pages( \@post_pages );
520              
521             my @all_pages = ( $self->index( \@post_pages ), $self->tag_pages( \%tag_pages ), @pages );
522             return @all_pages;
523             };
524              
525             #pod =method tags
526             #pod
527             #pod my @links = $app->tags;
528             #pod
529             #pod Get a set of L<link objects|Statocles::Link> suitable for creating a list of
530             #pod tag links. The common attributes are:
531             #pod
532             #pod text => 'The tag text'
533             #pod href => 'The URL to the tag page'
534             #pod
535             #pod =cut
536              
537             sub tags {
538 377     377 1 116925 my ( $self ) = @_;
539 377         788 my %tags;
540 377 50       802 my @pages = @{ $self->_post_pages || [] };
  377         6309  
541 377         3978 for my $page ( @pages ) {
542 1475         2035 for my $tag ( @{ $page->document->tags } ) {
  1475         22444  
543 2187   66     14561 $tags{ lc $tag } ||= $tag;
544             }
545             }
546 1477         57095 return map {; $self->link( text => $_, href => join( "/", 'tag', $self->_tag_url( $_ ), '' ) ) }
547 377         4038 map { $tags{ $_ } }
  1477         3170  
548             sort keys %tags;
549             }
550              
551             sub _tag_url {
552 2493     2493   5347 my ( $self, $tag ) = @_;
553 2493         5500 return lc $self->make_slug( $tag );
554             }
555              
556             #pod =method recent_posts
557             #pod
558             #pod my @pages = $app->recent_posts( $count, %filter );
559             #pod
560             #pod Get the last $count recent posts for this blog. Useful for templates and site
561             #pod index pages.
562             #pod
563             #pod %filter is an optional set of filters to apply to only show recent posts
564             #pod matching the given criteria. The following filters are available:
565             #pod
566             #pod =over 4
567             #pod
568             #pod =item tags
569             #pod
570             #pod (string) Only show posts with the given tag
571             #pod
572             #pod =back
573             #pod
574             #pod =cut
575              
576             sub recent_posts {
577 2     2 1 13137 my ( $self, $count, %filter ) = @_;
578              
579 2         9 my $root = $self->url_root;
580 2 100       34 my @pages = $self->_has_cached_post_pages ? @{ $self->_post_pages } : $self->pages;
  1         27  
581 2         11 my @found_pages;
582 2         5 PAGE: for my $page ( @pages ) {
583 23 100       520 next PAGE unless $page->path =~ qr{^$root/?(\d{4})/(\d{2})/(\d{2})/[^/]+(?:/index)?[.]html$};
584              
585 6         94 QUERY: for my $attr ( keys %filter ) {
586 4         7 my $value = $filter{ $attr };
587 4 50       11 if ( $attr eq 'tags' ) {
588 4 100       5 next PAGE unless grep { $_ eq $value } @{ $page->document->tags };
  6         31  
  4         61  
589             }
590             }
591              
592 3         5 push @found_pages, $page;
593 3 100       9 last if @found_pages >= $count;
594             }
595              
596 2         66 return @found_pages;
597             }
598              
599             #pod =method page_url
600             #pod
601             #pod my $url = $app->page_url( $page )
602             #pod
603             #pod Return the absolute URL to this L<page object|Statocles::Page>, removing the
604             #pod "/index.html" if necessary.
605             #pod
606             #pod =cut
607              
608             # XXX This is TERRIBLE. We need to do this better. Perhaps a "url()" helper in the
609             # template? And a full_url() helper? Or perhaps the template knows whether it should
610             # use absolute (/whatever) or full (http://www.example.com/whatever) URLs?
611              
612             sub page_url {
613 360     360 1 254909 my ( $self, $page ) = @_;
614 360         3006 my $url = "".$page->path;
615 360         5726 $url =~ s{/index[.]html$}{/};
616 360         1122 return $url;
617             }
618              
619             #=sub _sort_list
620             #
621             # my @sorted_pages = _sort_page_list( @unsorted_pages );
622             #
623             # Sort a list of blog post pages into buckets according to the date
624             # component of their path, and then sort the buckets according to the
625             # date field in the document.
626             #
627             # This allows a user to order the posts in a single day themselves,
628             # predictably and consistently.
629              
630             sub _sort_page_list {
631 842         16751 return map { $_->[0] }
632 893 50       26871 sort { $b->[1] cmp $a->[1] || $b->[2] cmp $a->[2] }
633 295     295   669 map { [ $_, $_->path =~ m{/(\d{4}/\d{2}/\d{2})}, $_->date ] }
  842         187617  
634             @_;
635             }
636              
637             1;
638              
639             __END__
640              
641             =pod
642              
643             =encoding UTF-8
644              
645             =head1 NAME
646              
647             Statocles::App::Blog - A blog application
648              
649             =head1 VERSION
650              
651             version 0.084
652              
653             =head1 DESCRIPTION
654              
655             This is a simple blog application for Statocles.
656              
657             =head2 FEATURES
658              
659             =over
660              
661             =item *
662              
663             Content dividers. By dividing your main content with "---", you create
664             sections. Only the first section will show up on the index page or in RSS
665             feeds.
666              
667             =item *
668              
669             RSS and Atom syndication feeds.
670              
671             =item *
672              
673             Tags to organize blog posts. Tags have their own custom feeds so users can
674             subscribe to only those posts they care about.
675              
676             =item *
677              
678             Cross-post links to redirect users to a syndicated blog. Useful when you
679             participate in many blogs and want to drive traffic to them.
680              
681             =item *
682              
683             Post-dated blog posts to appear automatically when the date is passed. If a
684             blog post is set in the future, it will not be added to the site when running
685             C<build> or C<deploy>.
686              
687             In order to ensure that post-dated blogs get added, you may want to run
688             C<deploy> in a nightly cron job.
689              
690             =back
691              
692             =head1 ATTRIBUTES
693              
694             =head2 store
695              
696             # site.yml
697             blog:
698             class: Statocles::App::Blog
699             args:
700             store: _posts
701              
702             The L<store directory path|Statocles::Store> to read for blog posts. Required.
703              
704             The Blog directory is organized in a tree by date, with a directory for the
705             year, month, day, and post. Each blog post is its own directory to allow for
706             additional files for the post, like images or additional pages.
707              
708             =head2 tag_text
709              
710             # site.yml
711             blog:
712             class: Statocles::App::Blog
713             args:
714             tag_text:
715             software: Posts about software and development
716             travel: My travelogue around the world!
717              
718             A hash of tag and introductory Markdown that will be shown on the tag's main
719             page. Having a description is optional.
720              
721             Using L<Beam::Wire's $config directive|Beam::Wire/Config Services>, you can
722             put the tag text in an external file:
723              
724             # site.yml
725             blog:
726             class: Statocles::App::Blog
727             args:
728             tag_text:
729             $config: tags.yml
730              
731             # tags.yml
732             software: |-
733             # Software
734              
735             Posts about software development, mostly in [Perl](http://perl.org)
736              
737             travel: |-
738             # Travel
739              
740             My travelogue around the world! [Also visit my Instagram!](http://example.com)
741              
742             =head2 page_size
743              
744             # site.yml
745             blog:
746             class: Statocles::App::Blog
747             args:
748             page_size: 5
749              
750             The number of posts to put in a page (the main page and the tag pages). Defaults
751             to 5.
752              
753             =head2 index_tags
754              
755             # site.yml
756             blog:
757             class: Statocles::App::Blog
758             args:
759             index_tags: [ '-private', '+important' ]
760              
761             Filter the tags shown in the index page. An array of tags prefixed with either
762             a + or a -. By prefixing the tag with a "-", it will be removed from the index,
763             unless a later tag prefixed with a "+" also matches.
764              
765             By default, all tags are shown on the index page.
766              
767             So, given a document with tags "foo", and "bar":
768              
769             index_tags: [ ] # document will be included
770             index_tags: [ '-foo' ] # document will not be included
771             index_tags: [ '-foo', '+bar' ] # document will be included
772              
773             =head2 template_dir
774              
775             The directory (inside the theme directory) to use for this app's templates.
776             Defaults to C<blog>.
777              
778             =head1 METHODS
779              
780             =head2 command
781              
782             my $exitval = $app->command( $app_name, @args );
783              
784             Run a command on this app. The app name is used to build the help, so
785             users get exactly what they need to run.
786              
787             =head2 make_slug
788              
789             my $slug = $app->make_slug( $title );
790              
791             Given a post title, remove special characters to create a slug.
792              
793             =head2 index
794              
795             my @pages = $app->index( \@post_pages );
796              
797             Build the index page (a L<list page|Statocles::Page::List>) and all related
798             feed pages out of the given array reference of post pages.
799              
800             =head2 tag_pages
801              
802             my @pages = $app->tag_pages( \%tag_pages );
803              
804             Get L<pages|Statocles::Page> for the tags in the given blog post documents
805             (build from L<the post_pages method|/post_pages>, including relevant feed
806             pages.
807              
808             =head2 pages
809              
810             my @pages = $app->pages( %options );
811              
812             Get all the L<pages|Statocles::Page> for this application. Available options
813             are:
814              
815             =over 4
816              
817             =item date
818              
819             The date to build for. Only posts on or before this date will be built.
820             Defaults to the current date.
821              
822             =back
823              
824             =head2 tags
825              
826             my @links = $app->tags;
827              
828             Get a set of L<link objects|Statocles::Link> suitable for creating a list of
829             tag links. The common attributes are:
830              
831             text => 'The tag text'
832             href => 'The URL to the tag page'
833              
834             =head2 recent_posts
835              
836             my @pages = $app->recent_posts( $count, %filter );
837              
838             Get the last $count recent posts for this blog. Useful for templates and site
839             index pages.
840              
841             %filter is an optional set of filters to apply to only show recent posts
842             matching the given criteria. The following filters are available:
843              
844             =over 4
845              
846             =item tags
847              
848             (string) Only show posts with the given tag
849              
850             =back
851              
852             =head2 page_url
853              
854             my $url = $app->page_url( $page )
855              
856             Return the absolute URL to this L<page object|Statocles::Page>, removing the
857             "/index.html" if necessary.
858              
859             =head1 COMMANDS
860              
861             =head2 post
862              
863             post [--date <date>] <title>
864              
865             Create a new blog post, optionally setting an initial C<title>. The post will be
866             created in a directory according to the current date.
867              
868             Initial post content can be read from C<STDIN>. This lets you write other programs
869             to generate content for blog posts (for example, to help automate release blog posts).
870              
871             =head1 THEME
872              
873             =over
874              
875             =item index.html
876              
877             The index page template. Gets the following template variables:
878              
879             =over
880              
881             =item site
882              
883             The L<Statocles::Site> object.
884              
885             =item pages
886              
887             An array reference containing all the blog post pages. Each page is a hash reference with the following keys:
888              
889             =over
890              
891             =item content
892              
893             The post content
894              
895             =item title
896              
897             The post title
898              
899             =item author
900              
901             The post author
902              
903             =back
904              
905             =item post.html
906              
907             The main post page template. Gets the following template variables:
908              
909             =over
910              
911             =item site
912              
913             The L<Statocles::Site> object
914              
915             =item content
916              
917             The post content
918              
919             =item title
920              
921             The post title
922              
923             =item author
924              
925             The post author
926              
927             =back
928              
929             =back
930              
931             =back
932              
933             =head1 SEE ALSO
934              
935             =over 4
936              
937             =item L<Statocles::App>
938              
939             =back
940              
941             =head1 AUTHOR
942              
943             Doug Bell <preaction@cpan.org>
944              
945             =head1 COPYRIGHT AND LICENSE
946              
947             This software is copyright (c) 2016 by Doug Bell.
948              
949             This is free software; you can redistribute it and/or modify it under
950             the same terms as the Perl 5 programming language system itself.
951              
952             =cut