File Coverage

blib/lib/Statocles/Site.pm
Criterion Covered Total %
statement 164 168 97.6
branch 49 54 90.7
condition 25 32 78.1
subroutine 18 19 94.7
pod 8 8 100.0
total 264 281 93.9


line stmt bran cond sub pod time code
1             package Statocles::Site;
2             our $VERSION = '0.086';
3             # ABSTRACT: An entire, configured website
4              
5 58     58   53476 use Statocles::Base 'Class', 'Emitter';
  58         131  
  58         756  
6 58     58   3671 use Scalar::Util qw( blessed );
  58         131  
  58         3639  
7 58     58   29388 use Text::Markdown;
  58         781748  
  58         3060  
8 58     58   18796 use Mojo::URL;
  58         293094  
  58         586  
9 58     58   15979 use Mojo::Log;
  58         461552  
  58         565  
10 58     58   20026 use Statocles::Page::Plain;
  58         236  
  58         2994  
11 58     58   538 use Statocles::Util qw( derp );
  58         228  
  58         3528  
12 58     58   22784 use List::UtilsBy qw( uniq_by );
  58         78898  
  58         175376  
13              
14             #pod =attr title
15             #pod
16             #pod The site title, used in templates.
17             #pod
18             #pod =cut
19              
20             has title => (
21             is => 'ro',
22             isa => Str,
23             default => sub { '' },
24             );
25              
26             #pod =attr author
27             #pod
28             #pod author: Doug Bell <doug@example.com>
29             #pod author:
30             #pod name: Doug Bell
31             #pod email: doug@example.com
32             #pod
33             #pod The primary author of the site, which will be used as the default author
34             #pod for all content. This can be a string with the author's name, and an
35             #pod optional e-mail address wrapped in E<lt>E<gt>, or a hashref of
36             #pod L<Statocles::Person attributes|Statocles::Person/ATTRIBUTES>.
37             #pod
38             #pod Individual documents can have their own authors. See
39             #pod L<Statocles::Document/author>.
40             #pod
41             #pod =cut
42              
43             has author => (
44             is => 'ro',
45             isa => Person,
46             coerce => Person->coercion,
47             );
48              
49             #pod =attr base_url
50             #pod
51             #pod The base URL of the site, including protocol and domain. Used mostly for feeds.
52             #pod
53             #pod This can be overridden by L<base_url in Deploy|Statocles::Deploy/base_url>.
54             #pod
55             #pod =cut
56              
57             has base_url => (
58             is => 'ro',
59             isa => Str,
60             default => sub { '/' },
61             );
62              
63             #pod =attr theme
64             #pod
65             #pod The L<theme|Statocles::Theme> for this site. All apps share the same theme.
66             #pod
67             #pod =cut
68              
69             has theme => (
70             is => 'ro',
71             isa => Theme,
72             coerce => Theme->coercion,
73             default => sub {
74             require Statocles::Theme;
75             Statocles::Theme->new( store => '::default' );
76             },
77             );
78              
79             #pod =attr apps
80             #pod
81             #pod The applications in this site. Each application has a name
82             #pod that can be used later.
83             #pod
84             #pod =cut
85              
86             has apps => (
87             is => 'ro',
88             isa => HashRef[ConsumerOf['Statocles::App']],
89             default => sub { {} },
90             );
91              
92             #pod =attr plugins
93             #pod
94             #pod The plugins in this site. Each plugin has a name that can be used later.
95             #pod
96             #pod =cut
97              
98             has plugins => (
99             is => 'ro',
100             isa => HashRef[ConsumerOf['Statocles::Plugin']],
101             default => sub { {} },
102             );
103              
104             #pod =attr index
105             #pod
106             #pod The page path to use for the site index. Make sure to include the leading slash
107             #pod (but C</index.html> is optional). Defaults to C</>, so any app with C<url_root>
108             #pod of C</> will be the index.
109             #pod
110             #pod =cut
111              
112             has index => (
113             is => 'ro',
114             isa => Str,
115             default => sub { '/' },
116             );
117              
118             #pod =attr nav
119             #pod
120             #pod Named navigation lists. A hash of arrays of hashes with the following keys:
121             #pod
122             #pod title - The title of the link
123             #pod href - The href of the link
124             #pod
125             #pod The most likely name for your navigation will be C<main>. Navigation names
126             #pod are defined by your L<theme|Statocles::Theme>. For example:
127             #pod
128             #pod {
129             #pod main => [
130             #pod {
131             #pod title => 'Blog',
132             #pod href => '/blog',
133             #pod },
134             #pod {
135             #pod title => 'Contact',
136             #pod href => '/contact.html',
137             #pod },
138             #pod ],
139             #pod }
140             #pod
141             #pod =cut
142              
143             has _nav => (
144             is => 'ro',
145             isa => LinkHash,
146             coerce => LinkHash->coercion,
147             default => sub { {} },
148             init_arg => 'nav',
149             );
150              
151             #pod =attr links
152             #pod
153             #pod # site.yml
154             #pod links:
155             #pod stylesheet:
156             #pod - href: /theme/css/site.css
157             #pod script:
158             #pod - href: /theme/js/site.js
159             #pod
160             #pod Related links for this site. Links are used to build relationships
161             #pod to other web addresses. Link categories are named based on their
162             #pod relationship. Some possible categories are:
163             #pod
164             #pod =over 4
165             #pod
166             #pod =item stylesheet
167             #pod
168             #pod Additional stylesheets for this site.
169             #pod
170             #pod =item script
171             #pod
172             #pod Additional scripts for this site.
173             #pod
174             #pod =back
175             #pod
176             #pod Each category contains an arrayref of hashrefs of L<link objects|Statocles::Link>.
177             #pod See the L<Statocles::Link|Statocles::Link> documentation for a full list of
178             #pod supported attributes. The most common attributes are:
179             #pod
180             #pod =over 4
181             #pod
182             #pod =item href
183             #pod
184             #pod The URL for the link.
185             #pod
186             #pod =item text
187             #pod
188             #pod The text of the link. Not needed for stylesheet or script links.
189             #pod
190             #pod =back
191             #pod
192             #pod =cut
193              
194             has _links => (
195             is => 'ro',
196             isa => LinkHash,
197             default => sub { +{} },
198             coerce => LinkHash->coercion,
199             init_arg => 'links',
200             );
201              
202             #pod =attr images
203             #pod
204             #pod # site.yml
205             #pod images:
206             #pod icon: /images/icon.png
207             #pod
208             #pod Related images for this document. These are used by themes to display
209             #pod images in appropriate templates. Each image has a category, like
210             #pod C<title>, C<banner>, or C<icon>, mapped to an L<image
211             #pod object|Statocles::Image>. See the L<Statocles::Image|Statocles::Image>
212             #pod documentation for a full list of supported attributes. The most common
213             #pod attributes are:
214             #pod
215             #pod =over 4
216             #pod
217             #pod =item src
218             #pod
219             #pod The source path of the image. Relative paths will be resolved relative
220             #pod to this document.
221             #pod
222             #pod =item alt
223             #pod
224             #pod The alternative text to display if the image cannot be downloaded or
225             #pod rendered. Also the text to use for non-visual media.
226             #pod
227             #pod =back
228             #pod
229             #pod Useful image names are:
230             #pod
231             #pod =over 4
232             #pod
233             #pod =item icon
234             #pod
235             #pod The shortcut icon for the site.
236             #pod
237             #pod =back
238             #pod
239             #pod =cut
240              
241             has images => (
242             is => 'ro',
243             isa => HashRef[InstanceOf['Statocles::Image']],
244             default => sub { +{} },
245             coerce => sub {
246             my ( $ref ) = @_;
247             my %img;
248             for my $name ( keys %$ref ) {
249             my $attrs = $ref->{ $name };
250             if ( !ref $attrs ) {
251             $attrs = { src => $attrs };
252             }
253             $img{ $name } = Statocles::Image->new(
254             %{ $attrs },
255             );
256             }
257             return \%img;
258             },
259             );
260              
261             #pod =attr templates
262             #pod
263             #pod # site.yml
264             #pod templates:
265             #pod sitemap.xml: custom/sitemap.xml
266             #pod layout.html: custom/layout.html
267             #pod
268             #pod The custom templates to use for the site meta-template like
269             #pod C<sitemap.xml> and C<robots.txt>, or the site-wide default layout
270             #pod template. A mapping of template names to template paths (relative to the
271             #pod theme root directory).
272             #pod
273             #pod Developers should get site templates using L<the C<template>
274             #pod method|/template>.
275             #pod
276             #pod =cut
277              
278             has _templates => (
279             is => 'ro',
280             isa => HashRef,
281             default => sub { {} },
282             init_arg => 'templates',
283             );
284              
285             #pod =attr template_dir
286             #pod
287             #pod The directory (inside the theme directory) to use for the site meta-templates.
288             #pod
289             #pod =cut
290              
291             has template_dir => (
292             is => 'ro',
293             isa => Str,
294             default => sub { 'site' },
295             );
296              
297             #pod =attr build_store
298             #pod
299             #pod The L<store|Statocles::Store> object to use for C<build()>. This is a workspace
300             #pod and will be rebuilt often, using the C<build> and C<daemon> commands. This is
301             #pod also the store the C<daemon> command reads to serve the site.
302             #pod
303             #pod =cut
304              
305             has build_store => (
306             is => 'ro',
307             isa => Store,
308             default => sub {
309             my $path = Path::Tiny->new( '.statocles', 'build' );
310             if ( !$path->is_dir ) {
311             # Automatically make the build directory
312             $path->mkpath;
313             }
314             return Store->coercion->( $path );
315             },
316             coerce => sub {
317             my ( $arg ) = @_;
318             if ( !ref $arg && !-d $arg ) {
319             # Automatically make the build directory
320             Path::Tiny->new( $arg )->mkpath;
321             }
322             return Store->coercion->( $arg );
323             },
324             );
325              
326             #pod =attr deploy
327             #pod
328             #pod The L<deploy object|Statocles::Deploy> to use for C<deploy()>. This is
329             #pod intended to be the production deployment of the site. A build gets promoted to
330             #pod production by using the C<deploy> command.
331             #pod
332             #pod =cut
333              
334             has _deploy => (
335             is => 'ro',
336             isa => ConsumerOf['Statocles::Deploy'],
337             required => 1,
338             init_arg => 'deploy',
339             coerce => sub {
340             if ( ( blessed $_[0] && $_[0]->isa( 'Path::Tiny' ) ) || !ref $_[0] ) {
341             require Statocles::Deploy::File;
342             return Statocles::Deploy::File->new(
343             path => $_[0],
344             );
345             }
346             return $_[0];
347             },
348             );
349              
350             #pod =attr data
351             #pod
352             #pod A hash of arbitrary data available to theme templates. This is a good place to
353             #pod put extra structured data like social network links or make easy customizations
354             #pod to themes like header image URLs.
355             #pod
356             #pod =cut
357              
358             has data => (
359             is => 'ro',
360             isa => HashRef,
361             default => sub { {} },
362             );
363              
364             #pod =attr log
365             #pod
366             #pod A L<Mojo::Log> object to write logs to. Defaults to STDERR.
367             #pod
368             #pod =cut
369              
370             has log => (
371             is => 'ro',
372             isa => InstanceOf['Mojo::Log'],
373             lazy => 1,
374             default => sub {
375             Mojo::Log->new( level => 'warn' );
376             },
377             );
378              
379             #pod =attr markdown
380             #pod
381             #pod The Text::Markdown object to use to turn Markdown into HTML. Defaults to a
382             #pod plain Text::Markdown object.
383             #pod
384             #pod Any object with a "markdown" method will work here.
385             #pod
386             #pod =cut
387              
388             has markdown => (
389             is => 'ro',
390             isa => HasMethods['markdown'],
391             default => sub { Text::Markdown->new },
392             );
393              
394             #pod =attr disable_content_template
395             #pod
396             #pod This disables processing the content as a template. This can speed up processing
397             #pod when the content is not using template directives.
398             #pod
399             #pod This can be also set in the application
400             #pod (L<Statocles::App/disable_content_template>), or for each document
401             #pod (L<Statocles::Document/disable_content_template>).
402             #pod
403             #pod =cut
404              
405             has disable_content_template => (
406             is => 'ro',
407             isa => Bool,
408             lazy => 1,
409             default => 0,
410             predicate => 'has_disable_content_template',
411             );
412              
413             # The current deploy we're writing to
414             has _write_deploy => (
415             is => 'rw',
416             isa => ConsumerOf['Statocles::Deploy'],
417             clearer => '_clear_write_deploy',
418             );
419              
420             #pod =attr pages
421             #pod
422             #pod A cache of all the pages that the site contains. This is generated
423             #pod during the C<build> phase and is available to all the templates
424             #pod while they are being rendered.
425             #pod
426             #pod =cut
427              
428             has pages => (
429             is => 'rw',
430             isa => ArrayRef[ConsumerOf['Statocles::Page']],
431             default => sub { [] },
432             );
433              
434             #pod =method BUILD
435             #pod
436             #pod Register this site as the global site.
437             #pod
438             #pod =cut
439              
440             sub BUILD {
441 123     123 1 22765 my ( $self ) = @_;
442              
443 123         929 $Statocles::SITE = $self;
444 123         7098 for my $app ( values %{ $self->apps } ) {
  123         798  
445 93         2911 $app->site( $self );
446             }
447 123         1387 for my $plugin ( values %{ $self->plugins } ) {
  123         1897  
448 11         61 $plugin->register( $self );
449             }
450             }
451              
452             #pod =method app
453             #pod
454             #pod my $app = $site->app( $name );
455             #pod
456             #pod Get the app with the given C<name>.
457             #pod
458             #pod =cut
459              
460             sub app {
461 10     10 1 21217 my ( $self, $name ) = @_;
462 10         296 return $self->apps->{ $name };
463             }
464              
465             #pod =method nav
466             #pod
467             #pod my @links = $site->nav( $key );
468             #pod
469             #pod Get the list of links for the given nav C<key>. Each link is a
470             #pod L<Statocles::Link> object.
471             #pod
472             #pod title - The title of the link
473             #pod href - The href of the link
474             #pod
475             #pod If the named nav does not exist, returns an empty list.
476             #pod
477             #pod =cut
478              
479             sub nav {
480 605     605 1 9859 my ( $self, $name ) = @_;
481 605 100       3048 return $self->_nav->{ $name } ? @{ $self->_nav->{ $name } } : ();
  84         364  
482             }
483              
484             #pod =method build
485             #pod
486             #pod $site->build( %options );
487             #pod
488             #pod Build the site in its build location. The C<%options> hash is passed in to every
489             #pod app's C<pages> method, allowing for customization of app behavior based on
490             #pod command-line.
491             #pod
492             #pod =cut
493              
494             our %PAGE_PRIORITY = (
495             'Statocles::Page::File' => -100,
496             );
497              
498             sub build {
499 49     49 1 157020 my ( $self, %options ) = @_;
500              
501 49         213 my $store = $self->build_store;
502              
503             # Remove all pages from the build directory first
504 49         326 $_->remove_tree for $store->path->children;
505              
506 49         1321434 my $apps = $self->apps;
507 49         137 my @pages;
508             my %seen_paths;
509              
510             # Collect all the pages for this site
511             # XXX: Should we allow sites without indexes?
512 49         231 my $index_path = $self->index;
513 49         103 my $index_orig_path;
514 49 100 66     508 if ( $index_path && $index_path !~ m{^/} ) {
515 2         51 $self->log->warn(
516             sprintf 'site "index" property should be absolute path to index page (got "%s")',
517             $self->index,
518             );
519             }
520              
521 49         827 for my $app_name ( keys %{ $apps } ) {
  49         204  
522 87         447 my $app = $apps->{$app_name};
523 87         1357 my $index_path_re = qr{^$index_path(?:/index[.]html)?$};
524 87 100       482 if ( $app->DOES( 'Statocles::App::Role::Store' ) ) {
525             # Allow index to be path to document and not the resulting page
526             # (so, ending in ".markdown" or ".md")
527 83         2285 my $doc_path = $index_path;
528 83         164 my $doc_ext = join '|', @{ $app->store->document_extensions };
  83         593  
529 83         613 $doc_path =~ s/$doc_ext/html/;
530 83         770 $index_path_re = qr{^$doc_path(?:/index[.]html)?$};
531             }
532              
533 87         2352 my @app_pages = $app->pages( %options );
534              
535             # DEPRECATED: Index as app name
536 87 100       431 if ( $app_name eq $index_path ) {
537              
538 2 100       26 die sprintf 'ERROR: Index app "%s" did not generate any pages' . "\n", $self->index
539             unless @app_pages;
540              
541             # Rename the app's page so that we don't get two pages with identical
542             # content, which is bad for SEO
543 1         31 $app_pages[0]->path( '/index.html' );
544             }
545              
546 86         369 for my $page ( @app_pages ) {
547 1261         20144 my $path = $page->path;
548              
549 1261 100       8088 if ( $path =~ $index_path_re ) {
550             # Rename the app's page so that we don't get two pages with identical
551             # content, which is bad for SEO
552 25         619 $self->log->debug(
553             sprintf 'Found index page "%s" from app "%s"',
554             $path,
555             $app_name,
556             );
557 25         1460 $path = '/index.html';
558 25         408 $index_orig_path = $page->path;
559 25         424 $page->path( '/index.html' );
560             }
561              
562 1261 100       9070 if ( $seen_paths{ $path }{ $app_name } ) {
563 1         18 $self->log->warn(
564             sprintf 'Duplicate page with path "%s" from app "%s"',
565             $path,
566             $app_name,
567             );
568 1         187 next;
569             }
570              
571 1260         6159 $seen_paths{ $path }{ $app_name } = $page;
572             }
573             }
574              
575             # XXX: Do we want to allow sites with no index page ever?
576 48 100 66     703 if ( $self->index && !exists $seen_paths{ '/index.html' } ) {
577 2         6 my $index_document = $self->index;
578 2 100       11 unless ( $index_document =~ s{[.]html?}{.markdown} ) {
579 1         3 $index_document .= '/index.markdown';
580             }
581 2         33 die sprintf qq{ERROR: Index path "%s" does not exist. Do you need to create "%s"?},
582             $self->index,
583             $index_document;
584             }
585              
586 46         456 for my $path ( keys %seen_paths ) {
587 1258         1383 my %seen_apps = %{ $seen_paths{$path} };
  1258         3000  
588             # Warn about pages generated by more than one app
589 1258 100       2098 if ( keys %seen_apps > 1 ) {
590 4         11 my @seen_app_names = map { $_->[0] }
591 2         9 sort { $b->[1] <=> $a->[1] }
592 2   100     8 map { [ $_, $PAGE_PRIORITY{ ref $seen_apps{ $_ } } || 0 ] }
  4         33  
593             keys %seen_apps
594             ;
595              
596 2         45 $self->log->warn(
597             sprintf 'Duplicate page "%s" from apps: %s. Using %s',
598             $path,
599             join( ", ", @seen_app_names ),
600             $seen_app_names[0],
601             );
602              
603 2         607 push @pages, $seen_apps{ $seen_app_names[0] };
604             }
605             else {
606 1256         2259 push @pages, values %seen_apps;
607             }
608             }
609              
610             $self->emit(
611 46         352 'collect_pages',
612             class => 'Statocles::Event::Pages',
613             pages => \@pages,
614             );
615              
616             # @pages should not change after this, because it is being cached
617 46         49406 $self->pages( \@pages );
618              
619 46         30916 $self->emit(
620             'before_build_write',
621             class => 'Statocles::Event::Pages',
622             pages => \@pages,
623             );
624              
625             # Rewrite page content to add base URL
626 46         48345 my $base_url = $self->base_url;
627 46 100       823 if ( $self->_write_deploy ) {
628 14   66     320 $base_url = $self->_write_deploy->base_url || $base_url;
629             }
630 46         879 my $base_path = Mojo::URL->new( $base_url )->path;
631 46         9782 $base_path =~ s{/$}{};
632              
633             # DEPRECATED: Index without leading / is an index app
634             my $index_root = $self->index =~ m{^/} ? $self->index
635 46 50       11100 : $self->index ? $apps->{ $self->index }->url_root : '';
    100          
636 46         165 $index_root =~ s{/index[.]html$}{};
637              
638 46         128 for my $page ( @pages ) {
639 1259         31668 my $is_index = $page->path eq '/index.html';
640              
641 1259 100       17607 if ( !$page->has_dom ) {
642 383         5900 $store->write_file( $page->path, $page->render );
643 383         1274 next;
644             }
645              
646 876         14797 my $dom = $page->dom;
647 876         3275170 for my $attr ( qw( src href ) ) {
648 1752         517561 for my $el ( $dom->find( "[$attr]" )->each ) {
649 9007         1847295 my $url = $el->attr( $attr );
650              
651             # Fix relative links on the index page
652 9007 100 100     133219 if ( $is_index && $index_orig_path && $url !~ m{^([A-Za-z]+:|/)} ) {
      100        
653 18         87 $url = join "/", $index_orig_path->parent, $url;
654             }
655              
656 9007 100       29175 next unless $url =~ m{^/(?:[^/]|$)};
657              
658             # Rewrite links to the index app's index page
659 6760 100 66     33906 if ( $index_root && $url =~ m{^$index_root(?:/index[.]html)?$} ) {
660 338         726 $url = '/';
661             }
662              
663 6760 100       15161 if ( $base_path =~ /\S/ ) {
664 409         10532 $url = join "", $base_path, $url;
665             }
666              
667 6760         73366 $el->attr( $attr, $url );
668             }
669             }
670              
671 876         27858 $store->write_file( $page->path, $dom->to_string );
672             }
673              
674             # Build the sitemap.xml
675             # html files only
676             # sorted by path to keep order and prevent spurious deploy commits
677 639         818 my @indexed_pages = map { $_->[0] }
678 1998         2399 sort { $a->[1] cmp $b->[1] }
679 639         9297 map { [ $_, $self->url( $_->path ) ] }
680 46         192 grep { $_->path =~ /[.]html?$/ }
  1259         26057  
681             @pages;
682 46         291 my $tmpl = $self->template( 'sitemap.xml' );
683 46         4417 my $sitemap = Statocles::Page::Plain->new(
684             path => '/sitemap.xml',
685             content => $tmpl->render( site => $self, pages => \@indexed_pages ),
686             );
687 46         2302 push @pages, $sitemap;
688 46         233 $store->write_file( 'sitemap.xml', $sitemap->render );
689              
690             # robots.txt is the best way for crawlers to automatically discover sitemap.xml
691             # We should do more with this later...
692 46         243 my $robots_tmpl = $self->template( 'robots.txt' );
693 46         3588 my $robots = Statocles::Page::Plain->new(
694             path => '/robots.txt',
695             content => $robots_tmpl->render( site => $self ),
696             );
697 46         2040 push @pages, $robots;
698 46         239 $store->write_file( 'robots.txt', $robots->render );
699              
700             # Add the theme
701 46         1394 for my $page ( $self->theme->pages ) {
702 100         2581 push @pages, $page;
703 100         2124 $store->write_file( $page->path, $page->render );
704             }
705              
706 46         324 $self->emit( build => class => 'Statocles::Event::Pages', pages => \@pages );
707              
708 46         68102 return;
709             }
710              
711             sub _get_status {
712 0     0   0 my ( $self, $status ) = @_;
713 0         0 my $path = Path::Tiny->new( '.statocles', 'status.yml' );
714 0 0       0 return {} unless $path->exists;
715 0         0 YAML::Load( $path->slurp_utf8 );
716             }
717              
718             sub _write_status {
719 14     14   55 my ( $self, $status ) = @_;
720 14         111 Path::Tiny->new( '.statocles', 'status.yml' )->spew_utf8( YAML::Dump( $status ) );
721             }
722              
723             #pod =method deploy
724             #pod
725             #pod $site->deploy( %options );
726             #pod
727             #pod Deploy the site to its destination. The C<%options> are passed to the appropriate
728             #pod L<deploy object|Statocles::Deploy>.
729             #pod
730             #pod =cut
731              
732             sub deploy {
733 14     14 1 163696 my ( $self, %options ) = @_;
734 14         466 $self->_write_deploy( $self->_deploy );
735 14         1332 $self->build( %options );
736 14         355 $self->_deploy->site( $self );
737 14         614 $self->_deploy->deploy( $self->build_store, %options );
738 14         458 $self->_clear_write_deploy;
739 14         229 $self->_write_status( {
740             last_deploy_date => time(),
741             last_deploy_args => \%options,
742             } );
743             }
744              
745             #pod =method links
746             #pod
747             #pod my @links = $site->links( $key );
748             #pod my $link = $site->links( $key );
749             #pod $site->links( $key => $add_link );
750             #pod
751             #pod Get or append to the links set for the given key. See L<the links
752             #pod attribute|/links> for some commonly-used keys.
753             #pod
754             #pod If only one argument is given, returns a list of L<link
755             #pod objects|Statocles::Link>. In scalar context, returns the first link in
756             #pod the list.
757             #pod
758             #pod If two arguments are given, append the new link to the given key.
759             #pod C<$add_link> may be a URL string, a hash reference of L<link
760             #pod attributes|Statocles::Link/ATTRIBUTES>, or a L<Statocles::Link
761             #pod object|Statocles::Link>. When adding links, nothing is returned.
762             #pod
763             #pod =cut
764              
765             sub links {
766 1156     1156 1 69423 my ( $self, $name, $add_link ) = @_;
767 1156 100       2691 if ( $add_link ) {
768 2         3 push @{ $self->_links->{ $name } }, Link->coerce( $add_link );
  2         12  
769 2         60 return;
770             }
771 60     60   1239 my @links = uniq_by { $_->href }
772 1154 100       7697 $self->_links->{ $name } ? @{ $self->_links->{ $name } } : ();
  60         234  
773 1154 50       10064 return wantarray ? @links : $links[0];
774             }
775              
776             #pod =method url
777             #pod
778             #pod my $url = $site->url( $page_url );
779             #pod
780             #pod Get the full URL to the given path by prepending the C<base_url>.
781             #pod
782             #pod =cut
783              
784             sub url {
785 8921     8921 1 494437 my ( $self, $path ) = @_;
786 8921 100 100     118576 my $base = $self->_write_deploy && $self->_write_deploy->base_url
787             ? $self->_write_deploy->base_url
788             : $self->base_url;
789              
790             # Remove index.html from the end of the path, since it's redundant
791 8921         129671 $path =~ s{/index[.]html$}{/};
792              
793             # Remove the / from both sides of the join so we don't double up
794 8921         40468 $base =~ s{/$}{};
795 8921         23845 $path =~ s{^/}{};
796              
797 8921         37461 return join "/", $base, $path;
798             }
799              
800             #pod =method template
801             #pod
802             #pod my $template = $app->template( $tmpl_name );
803             #pod
804             #pod Get a L<template object|Statocles::Template> for the given template
805             #pod name. The default template is determined by the app's class name and the
806             #pod template name passed in.
807             #pod
808             #pod Applications should list the templates they have and describe what L<page
809             #pod class|Statocles::Page> they use.
810             #pod
811             #pod =cut
812              
813             sub template {
814 853     853 1 18441 my ( $self, @parts ) = @_;
815              
816 853 50       2491 if ( @parts == 1 ) {
817             @parts = $self->_templates->{ $parts[0] }
818 853 100       4589 ? $self->_templates->{ $parts[0] }
    100          
819             : $parts[0] eq 'layout.html'
820             ? ( 'layout', 'default.html' )
821             : ( $self->template_dir, @parts );
822             }
823              
824             # If the default layout doesn't exist, use the old default.
825             # Remove this in v2.0
826 853 100 66     7034 if ( $parts[0] eq 'layout' && $parts[1] eq 'default.html'
      66        
      66        
827             && !$self->theme->store->path->child( @parts )->is_file
828             && $self->theme->store->path->child( site => 'layout.html.ep' )->is_file
829             ) {
830 2         244 derp qq{Using default layout "site/layout.html.ep" is deprecated and will be removed in v2.0. Move your default layout to "layout/default.html.ep" to fix this warning.};
831 2         15 return $self->theme->template( qw( site layout.html ) );
832             }
833              
834 851         71674 return $self->theme->template( @parts );
835             }
836              
837             1;
838              
839             __END__
840              
841             =pod
842              
843             =encoding UTF-8
844              
845             =head1 NAME
846              
847             Statocles::Site - An entire, configured website
848              
849             =head1 VERSION
850              
851             version 0.086
852              
853             =head1 SYNOPSIS
854              
855             my $site = Statocles::Site->new(
856             title => 'My Site',
857             nav => [
858             { title => 'Home', href => '/' },
859             { title => 'Blog', href => '/blog' },
860             ],
861             apps => {
862             blog => Statocles::App::Blog->new( ... ),
863             },
864             );
865              
866             $site->deploy;
867              
868             =head1 DESCRIPTION
869              
870             A Statocles::Site is a collection of L<applications|Statocles::App>.
871              
872             =head1 ATTRIBUTES
873              
874             =head2 title
875              
876             The site title, used in templates.
877              
878             =head2 author
879              
880             author: Doug Bell <doug@example.com>
881             author:
882             name: Doug Bell
883             email: doug@example.com
884              
885             The primary author of the site, which will be used as the default author
886             for all content. This can be a string with the author's name, and an
887             optional e-mail address wrapped in E<lt>E<gt>, or a hashref of
888             L<Statocles::Person attributes|Statocles::Person/ATTRIBUTES>.
889              
890             Individual documents can have their own authors. See
891             L<Statocles::Document/author>.
892              
893             =head2 base_url
894              
895             The base URL of the site, including protocol and domain. Used mostly for feeds.
896              
897             This can be overridden by L<base_url in Deploy|Statocles::Deploy/base_url>.
898              
899             =head2 theme
900              
901             The L<theme|Statocles::Theme> for this site. All apps share the same theme.
902              
903             =head2 apps
904              
905             The applications in this site. Each application has a name
906             that can be used later.
907              
908             =head2 plugins
909              
910             The plugins in this site. Each plugin has a name that can be used later.
911              
912             =head2 index
913              
914             The page path to use for the site index. Make sure to include the leading slash
915             (but C</index.html> is optional). Defaults to C</>, so any app with C<url_root>
916             of C</> will be the index.
917              
918             =head2 nav
919              
920             Named navigation lists. A hash of arrays of hashes with the following keys:
921              
922             title - The title of the link
923             href - The href of the link
924              
925             The most likely name for your navigation will be C<main>. Navigation names
926             are defined by your L<theme|Statocles::Theme>. For example:
927              
928             {
929             main => [
930             {
931             title => 'Blog',
932             href => '/blog',
933             },
934             {
935             title => 'Contact',
936             href => '/contact.html',
937             },
938             ],
939             }
940              
941             =head2 links
942              
943             # site.yml
944             links:
945             stylesheet:
946             - href: /theme/css/site.css
947             script:
948             - href: /theme/js/site.js
949              
950             Related links for this site. Links are used to build relationships
951             to other web addresses. Link categories are named based on their
952             relationship. Some possible categories are:
953              
954             =over 4
955              
956             =item stylesheet
957              
958             Additional stylesheets for this site.
959              
960             =item script
961              
962             Additional scripts for this site.
963              
964             =back
965              
966             Each category contains an arrayref of hashrefs of L<link objects|Statocles::Link>.
967             See the L<Statocles::Link|Statocles::Link> documentation for a full list of
968             supported attributes. The most common attributes are:
969              
970             =over 4
971              
972             =item href
973              
974             The URL for the link.
975              
976             =item text
977              
978             The text of the link. Not needed for stylesheet or script links.
979              
980             =back
981              
982             =head2 images
983              
984             # site.yml
985             images:
986             icon: /images/icon.png
987              
988             Related images for this document. These are used by themes to display
989             images in appropriate templates. Each image has a category, like
990             C<title>, C<banner>, or C<icon>, mapped to an L<image
991             object|Statocles::Image>. See the L<Statocles::Image|Statocles::Image>
992             documentation for a full list of supported attributes. The most common
993             attributes are:
994              
995             =over 4
996              
997             =item src
998              
999             The source path of the image. Relative paths will be resolved relative
1000             to this document.
1001              
1002             =item alt
1003              
1004             The alternative text to display if the image cannot be downloaded or
1005             rendered. Also the text to use for non-visual media.
1006              
1007             =back
1008              
1009             Useful image names are:
1010              
1011             =over 4
1012              
1013             =item icon
1014              
1015             The shortcut icon for the site.
1016              
1017             =back
1018              
1019             =head2 templates
1020              
1021             # site.yml
1022             templates:
1023             sitemap.xml: custom/sitemap.xml
1024             layout.html: custom/layout.html
1025              
1026             The custom templates to use for the site meta-template like
1027             C<sitemap.xml> and C<robots.txt>, or the site-wide default layout
1028             template. A mapping of template names to template paths (relative to the
1029             theme root directory).
1030              
1031             Developers should get site templates using L<the C<template>
1032             method|/template>.
1033              
1034             =head2 template_dir
1035              
1036             The directory (inside the theme directory) to use for the site meta-templates.
1037              
1038             =head2 build_store
1039              
1040             The L<store|Statocles::Store> object to use for C<build()>. This is a workspace
1041             and will be rebuilt often, using the C<build> and C<daemon> commands. This is
1042             also the store the C<daemon> command reads to serve the site.
1043              
1044             =head2 deploy
1045              
1046             The L<deploy object|Statocles::Deploy> to use for C<deploy()>. This is
1047             intended to be the production deployment of the site. A build gets promoted to
1048             production by using the C<deploy> command.
1049              
1050             =head2 data
1051              
1052             A hash of arbitrary data available to theme templates. This is a good place to
1053             put extra structured data like social network links or make easy customizations
1054             to themes like header image URLs.
1055              
1056             =head2 log
1057              
1058             A L<Mojo::Log> object to write logs to. Defaults to STDERR.
1059              
1060             =head2 markdown
1061              
1062             The Text::Markdown object to use to turn Markdown into HTML. Defaults to a
1063             plain Text::Markdown object.
1064              
1065             Any object with a "markdown" method will work here.
1066              
1067             =head2 disable_content_template
1068              
1069             This disables processing the content as a template. This can speed up processing
1070             when the content is not using template directives.
1071              
1072             This can be also set in the application
1073             (L<Statocles::App/disable_content_template>), or for each document
1074             (L<Statocles::Document/disable_content_template>).
1075              
1076             =head2 pages
1077              
1078             A cache of all the pages that the site contains. This is generated
1079             during the C<build> phase and is available to all the templates
1080             while they are being rendered.
1081              
1082             =head1 METHODS
1083              
1084             =head2 BUILD
1085              
1086             Register this site as the global site.
1087              
1088             =head2 app
1089              
1090             my $app = $site->app( $name );
1091              
1092             Get the app with the given C<name>.
1093              
1094             =head2 nav
1095              
1096             my @links = $site->nav( $key );
1097              
1098             Get the list of links for the given nav C<key>. Each link is a
1099             L<Statocles::Link> object.
1100              
1101             title - The title of the link
1102             href - The href of the link
1103              
1104             If the named nav does not exist, returns an empty list.
1105              
1106             =head2 build
1107              
1108             $site->build( %options );
1109              
1110             Build the site in its build location. The C<%options> hash is passed in to every
1111             app's C<pages> method, allowing for customization of app behavior based on
1112             command-line.
1113              
1114             =head2 deploy
1115              
1116             $site->deploy( %options );
1117              
1118             Deploy the site to its destination. The C<%options> are passed to the appropriate
1119             L<deploy object|Statocles::Deploy>.
1120              
1121             =head2 links
1122              
1123             my @links = $site->links( $key );
1124             my $link = $site->links( $key );
1125             $site->links( $key => $add_link );
1126              
1127             Get or append to the links set for the given key. See L<the links
1128             attribute|/links> for some commonly-used keys.
1129              
1130             If only one argument is given, returns a list of L<link
1131             objects|Statocles::Link>. In scalar context, returns the first link in
1132             the list.
1133              
1134             If two arguments are given, append the new link to the given key.
1135             C<$add_link> may be a URL string, a hash reference of L<link
1136             attributes|Statocles::Link/ATTRIBUTES>, or a L<Statocles::Link
1137             object|Statocles::Link>. When adding links, nothing is returned.
1138              
1139             =head2 url
1140              
1141             my $url = $site->url( $page_url );
1142              
1143             Get the full URL to the given path by prepending the C<base_url>.
1144              
1145             =head2 template
1146              
1147             my $template = $app->template( $tmpl_name );
1148              
1149             Get a L<template object|Statocles::Template> for the given template
1150             name. The default template is determined by the app's class name and the
1151             template name passed in.
1152              
1153             Applications should list the templates they have and describe what L<page
1154             class|Statocles::Page> they use.
1155              
1156             =head1 EVENTS
1157              
1158             The site object exposes the following events.
1159              
1160             =head2 collect_pages
1161              
1162             This event is fired after all the pages have been collected, but before they
1163             have been rendered. This allows you to edit the page's data or add/remove
1164             pages from the list.
1165              
1166             The event will be a
1167             L<Statocles::Event::Pages|Statocles::Event/Statocles::Event::Pages> object
1168             containing all the pages built by the apps.
1169              
1170             =head2 before_build_write
1171              
1172             This event is fired after the pages have been built by the apps, but before
1173             any page is written to the C<build_store>.
1174              
1175             The event will be a
1176             L<Statocles::Event::Pages|Statocles::Event/Statocles::Event::Pages> object
1177             containing all the pages built by the apps.
1178              
1179             =head2 build
1180              
1181             This event is fired after the site has been built and the pages written to the
1182             C<build_store>.
1183              
1184             The event will be a
1185             L<Statocles::Event::Pages|Statocles::Event/Statocles::Event::Pages> object
1186             containing all the pages built by the site.
1187              
1188             =head1 AUTHOR
1189              
1190             Doug Bell <preaction@cpan.org>
1191              
1192             =head1 COPYRIGHT AND LICENSE
1193              
1194             This software is copyright (c) 2016 by Doug Bell.
1195              
1196             This is free software; you can redistribute it and/or modify it under
1197             the same terms as the Perl 5 programming language system itself.
1198              
1199             =cut