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.085';
3             # ABSTRACT: An entire, configured website
4              
5 58     58   49691 use Statocles::Base 'Class', 'Emitter';
  58         125  
  58         639  
6 58     58   3536 use Scalar::Util qw( blessed );
  58         133  
  58         3351  
7 58     58   28893 use Text::Markdown;
  58         790782  
  58         3253  
8 58     58   19240 use Mojo::URL;
  58         294705  
  58         525  
9 58     58   15946 use Mojo::Log;
  58         450268  
  58         618  
10 58     58   20007 use Statocles::Page::Plain;
  58         237  
  58         2992  
11 58     58   505 use Statocles::Util qw( derp );
  58         325  
  58         3381  
12 58     58   22517 use List::UtilsBy qw( uniq_by );
  58         77617  
  58         171372  
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             # The current deploy we're writing to
395             has _write_deploy => (
396             is => 'rw',
397             isa => ConsumerOf['Statocles::Deploy'],
398             clearer => '_clear_write_deploy',
399             );
400              
401             #pod =attr pages
402             #pod
403             #pod A cache of all the pages that the site contains. This is generated
404             #pod during the C<build> phase and is available to all the templates
405             #pod while they are being rendered.
406             #pod
407             #pod =cut
408              
409             has pages => (
410             is => 'rw',
411             isa => ArrayRef[ConsumerOf['Statocles::Page']],
412             default => sub { [] },
413             );
414              
415             #pod =method BUILD
416             #pod
417             #pod Register this site as the global site.
418             #pod
419             #pod =cut
420              
421             sub BUILD {
422 122     122 1 21308 my ( $self ) = @_;
423              
424 122         908 $Statocles::SITE = $self;
425 122         4388 for my $app ( values %{ $self->apps } ) {
  122         732  
426 93         2896 $app->site( $self );
427             }
428 122         1306 for my $plugin ( values %{ $self->plugins } ) {
  122         1944  
429 11         68 $plugin->register( $self );
430             }
431             }
432              
433             #pod =method app
434             #pod
435             #pod my $app = $site->app( $name );
436             #pod
437             #pod Get the app with the given C<name>.
438             #pod
439             #pod =cut
440              
441             sub app {
442 10     10 1 20175 my ( $self, $name ) = @_;
443 10         275 return $self->apps->{ $name };
444             }
445              
446             #pod =method nav
447             #pod
448             #pod my @links = $site->nav( $key );
449             #pod
450             #pod Get the list of links for the given nav C<key>. Each link is a
451             #pod L<Statocles::Link> object.
452             #pod
453             #pod title - The title of the link
454             #pod href - The href of the link
455             #pod
456             #pod If the named nav does not exist, returns an empty list.
457             #pod
458             #pod =cut
459              
460             sub nav {
461 605     605 1 9844 my ( $self, $name ) = @_;
462 605 100       3358 return $self->_nav->{ $name } ? @{ $self->_nav->{ $name } } : ();
  84         406  
463             }
464              
465             #pod =method build
466             #pod
467             #pod $site->build( %options );
468             #pod
469             #pod Build the site in its build location. The C<%options> hash is passed in to every
470             #pod app's C<pages> method, allowing for customization of app behavior based on
471             #pod command-line.
472             #pod
473             #pod =cut
474              
475             our %PAGE_PRIORITY = (
476             'Statocles::Page::File' => -100,
477             );
478              
479             sub build {
480 49     49 1 170908 my ( $self, %options ) = @_;
481              
482 49         211 my $store = $self->build_store;
483              
484             # Remove all pages from the build directory first
485 49         324 $_->remove_tree for $store->path->children;
486              
487 49         73224 my $apps = $self->apps;
488 49         162 my @pages;
489             my %seen_paths;
490              
491             # Collect all the pages for this site
492             # XXX: Should we allow sites without indexes?
493 49         215 my $index_path = $self->index;
494 49         114 my $index_orig_path;
495 49 100 66     542 if ( $index_path && $index_path !~ m{^/} ) {
496 2         43 $self->log->warn(
497             sprintf 'site "index" property should be absolute path to index page (got "%s")',
498             $self->index,
499             );
500             }
501              
502 49         622 for my $app_name ( keys %{ $apps } ) {
  49         215  
503 87         488 my $app = $apps->{$app_name};
504 87         1404 my $index_path_re = qr{^$index_path(?:/index[.]html)?$};
505 87 100       498 if ( $app->DOES( 'Statocles::App::Role::Store' ) ) {
506             # Allow index to be path to document and not the resulting page
507             # (so, ending in ".markdown" or ".md")
508 83         2310 my $doc_path = $index_path;
509 83         170 my $doc_ext = join '|', @{ $app->store->document_extensions };
  83         641  
510 83         597 $doc_path =~ s/$doc_ext/html/;
511 83         767 $index_path_re = qr{^$doc_path(?:/index[.]html)?$};
512             }
513              
514 87         2467 my @app_pages = $app->pages( %options );
515              
516             # DEPRECATED: Index as app name
517 87 100       427 if ( $app_name eq $index_path ) {
518              
519 2 100       19 die sprintf 'ERROR: Index app "%s" did not generate any pages' . "\n", $self->index
520             unless @app_pages;
521              
522             # Rename the app's page so that we don't get two pages with identical
523             # content, which is bad for SEO
524 1         20 $app_pages[0]->path( '/index.html' );
525             }
526              
527 86         354 for my $page ( @app_pages ) {
528 1261         20122 my $path = $page->path;
529              
530 1261 100       8091 if ( $path =~ $index_path_re ) {
531             # Rename the app's page so that we don't get two pages with identical
532             # content, which is bad for SEO
533 25         594 $self->log->debug(
534             sprintf 'Found index page "%s" from app "%s"',
535             $path,
536             $app_name,
537             );
538 25         1626 $path = '/index.html';
539 25         434 $index_orig_path = $page->path;
540 25         443 $page->path( '/index.html' );
541             }
542              
543 1261 100       9444 if ( $seen_paths{ $path }{ $app_name } ) {
544 1         19 $self->log->warn(
545             sprintf 'Duplicate page with path "%s" from app "%s"',
546             $path,
547             $app_name,
548             );
549 1         200 next;
550             }
551              
552 1260         6102 $seen_paths{ $path }{ $app_name } = $page;
553             }
554             }
555              
556             # XXX: Do we want to allow sites with no index page ever?
557 48 100 66     773 if ( $self->index && !exists $seen_paths{ '/index.html' } ) {
558 2         5 my $index_document = $self->index;
559 2 100       11 unless ( $index_document =~ s{[.]html?}{.markdown} ) {
560 1         3 $index_document .= '/index.markdown';
561             }
562 2         28 die sprintf qq{ERROR: Index path "%s" does not exist. Do you need to create "%s"?},
563             $self->index,
564             $index_document;
565             }
566              
567 46         535 for my $path ( keys %seen_paths ) {
568 1258         1412 my %seen_apps = %{ $seen_paths{$path} };
  1258         3013  
569             # Warn about pages generated by more than one app
570 1258 100       2093 if ( keys %seen_apps > 1 ) {
571 4         10 my @seen_app_names = map { $_->[0] }
572 2         9 sort { $b->[1] <=> $a->[1] }
573 2   100     6 map { [ $_, $PAGE_PRIORITY{ ref $seen_apps{ $_ } } || 0 ] }
  4         26  
574             keys %seen_apps
575             ;
576              
577 2         41 $self->log->warn(
578             sprintf 'Duplicate page "%s" from apps: %s. Using %s',
579             $path,
580             join( ", ", @seen_app_names ),
581             $seen_app_names[0],
582             );
583              
584 2         564 push @pages, $seen_apps{ $seen_app_names[0] };
585             }
586             else {
587 1256         2295 push @pages, values %seen_apps;
588             }
589             }
590              
591             $self->emit(
592 46         381 'collect_pages',
593             class => 'Statocles::Event::Pages',
594             pages => \@pages,
595             );
596              
597             # @pages should not change after this, because it is being cached
598 46         50697 $self->pages( \@pages );
599              
600 46         33038 $self->emit(
601             'before_build_write',
602             class => 'Statocles::Event::Pages',
603             pages => \@pages,
604             );
605              
606             # Rewrite page content to add base URL
607 46         48637 my $base_url = $self->base_url;
608 46 100       889 if ( $self->_write_deploy ) {
609 14   66     396 $base_url = $self->_write_deploy->base_url || $base_url;
610             }
611 46         1000 my $base_path = Mojo::URL->new( $base_url )->path;
612 46         10039 $base_path =~ s{/$}{};
613              
614             # DEPRECATED: Index without leading / is an index app
615             my $index_root = $self->index =~ m{^/} ? $self->index
616 46 50       7996 : $self->index ? $apps->{ $self->index }->url_root : '';
    100          
617 46         131 $index_root =~ s{/index[.]html$}{};
618              
619 46         143 for my $page ( @pages ) {
620 1259         33742 my $is_index = $page->path eq '/index.html';
621              
622 1259 100       19221 if ( !$page->has_dom ) {
623 383         5905 $store->write_file( $page->path, $page->render );
624 383         1300 next;
625             }
626              
627 876         15941 my $dom = $page->dom;
628 876         3617387 for my $attr ( qw( src href ) ) {
629 1752         585402 for my $el ( $dom->find( "[$attr]" )->each ) {
630 9029         2042665 my $url = $el->attr( $attr );
631              
632             # Fix relative links on the index page
633 9029 100 100     147030 if ( $is_index && $index_orig_path && $url !~ m{^([A-Za-z]+:|/)} ) {
      100        
634 18         102 $url = join "/", $index_orig_path->parent, $url;
635             }
636              
637 9029 100       32935 next unless $url =~ m{^/(?:[^/]|$)};
638              
639             # Rewrite links to the index app's index page
640 6779 100 66     37845 if ( $index_root && $url =~ m{^$index_root(?:/index[.]html)?$} ) {
641 338         789 $url = '/';
642             }
643              
644 6779 100       16138 if ( $base_path =~ /\S/ ) {
645 409         13580 $url = join "", $base_path, $url;
646             }
647              
648 6779         75878 $el->attr( $attr, $url );
649             }
650             }
651              
652 876         47879 $store->write_file( $page->path, $dom->to_string );
653             }
654              
655             # Build the sitemap.xml
656             # html files only
657             # sorted by path to keep order and prevent spurious deploy commits
658 639         915 my @indexed_pages = map { $_->[0] }
659 2008         2433 sort { $a->[1] cmp $b->[1] }
660 639         10000 map { [ $_, $self->url( $_->path ) ] }
661 46         238 grep { $_->path =~ /[.]html?$/ }
  1259         27163  
662             @pages;
663 46         304 my $tmpl = $self->template( 'sitemap.xml' );
664 46         5178 my $sitemap = Statocles::Page::Plain->new(
665             path => '/sitemap.xml',
666             content => $tmpl->render( site => $self, pages => \@indexed_pages ),
667             );
668 46         2523 push @pages, $sitemap;
669 46         276 $store->write_file( 'sitemap.xml', $sitemap->render );
670              
671             # robots.txt is the best way for crawlers to automatically discover sitemap.xml
672             # We should do more with this later...
673 46         277 my $robots_tmpl = $self->template( 'robots.txt' );
674 46         4189 my $robots = Statocles::Page::Plain->new(
675             path => '/robots.txt',
676             content => $robots_tmpl->render( site => $self ),
677             );
678 46         2095 push @pages, $robots;
679 46         219 $store->write_file( 'robots.txt', $robots->render );
680              
681             # Add the theme
682 46         1531 for my $page ( $self->theme->pages ) {
683 100         2733 push @pages, $page;
684 100         2182 $store->write_file( $page->path, $page->render );
685             }
686              
687 46         376 $self->emit( build => class => 'Statocles::Event::Pages', pages => \@pages );
688              
689 46         72409 return;
690             }
691              
692             sub _get_status {
693 0     0   0 my ( $self, $status ) = @_;
694 0         0 my $path = Path::Tiny->new( '.statocles', 'status.yml' );
695 0 0       0 return {} unless $path->exists;
696 0         0 YAML::Load( $path->slurp_utf8 );
697             }
698              
699             sub _write_status {
700 14     14   55 my ( $self, $status ) = @_;
701 14         130 Path::Tiny->new( '.statocles', 'status.yml' )->spew_utf8( YAML::Dump( $status ) );
702             }
703              
704             #pod =method deploy
705             #pod
706             #pod $site->deploy( %options );
707             #pod
708             #pod Deploy the site to its destination. The C<%options> are passed to the appropriate
709             #pod L<deploy object|Statocles::Deploy>.
710             #pod
711             #pod =cut
712              
713             sub deploy {
714 14     14 1 120790 my ( $self, %options ) = @_;
715 14         440 $self->_write_deploy( $self->_deploy );
716 14         1281 $self->build( %options );
717 14         452 $self->_deploy->site( $self );
718 14         682 $self->_deploy->deploy( $self->build_store, %options );
719 14         563 $self->_clear_write_deploy;
720 14         241 $self->_write_status( {
721             last_deploy_date => time(),
722             last_deploy_args => \%options,
723             } );
724             }
725              
726             #pod =method links
727             #pod
728             #pod my @links = $site->links( $key );
729             #pod my $link = $site->links( $key );
730             #pod $site->links( $key => $add_link );
731             #pod
732             #pod Get or append to the links set for the given key. See L<the links
733             #pod attribute|/links> for some commonly-used keys.
734             #pod
735             #pod If only one argument is given, returns a list of L<link
736             #pod objects|Statocles::Link>. In scalar context, returns the first link in
737             #pod the list.
738             #pod
739             #pod If two arguments are given, append the new link to the given key.
740             #pod C<$add_link> may be a URL string, a hash reference of L<link
741             #pod attributes|Statocles::Link/ATTRIBUTES>, or a L<Statocles::Link
742             #pod object|Statocles::Link>. When adding links, nothing is returned.
743             #pod
744             #pod =cut
745              
746             sub links {
747 1156     1156 1 84177 my ( $self, $name, $add_link ) = @_;
748 1156 100       3271 if ( $add_link ) {
749 2         3 push @{ $self->_links->{ $name } }, Link->coerce( $add_link );
  2         14  
750 2         72 return;
751             }
752 60     60   1366 my @links = uniq_by { $_->href }
753 1154 100       9834 $self->_links->{ $name } ? @{ $self->_links->{ $name } } : ();
  60         284  
754 1154 50       10808 return wantarray ? @links : $links[0];
755             }
756              
757             #pod =method url
758             #pod
759             #pod my $url = $site->url( $page_url );
760             #pod
761             #pod Get the full URL to the given path by prepending the C<base_url>.
762             #pod
763             #pod =cut
764              
765             sub url {
766 8921     8921 1 559376 my ( $self, $path ) = @_;
767 8921 100 100     139983 my $base = $self->_write_deploy && $self->_write_deploy->base_url
768             ? $self->_write_deploy->base_url
769             : $self->base_url;
770              
771             # Remove index.html from the end of the path, since it's redundant
772 8921         142090 $path =~ s{/index[.]html$}{/};
773              
774             # Remove the / from both sides of the join so we don't double up
775 8921         43982 $base =~ s{/$}{};
776 8921         25324 $path =~ s{^/}{};
777              
778 8921         38921 return join "/", $base, $path;
779             }
780              
781             #pod =method template
782             #pod
783             #pod my $template = $app->template( $tmpl_name );
784             #pod
785             #pod Get a L<template object|Statocles::Template> for the given template
786             #pod name. The default template is determined by the app's class name and the
787             #pod template name passed in.
788             #pod
789             #pod Applications should list the templates they have and describe what L<page
790             #pod class|Statocles::Page> they use.
791             #pod
792             #pod =cut
793              
794             sub template {
795 853     853 1 18522 my ( $self, @parts ) = @_;
796              
797 853 50       2297 if ( @parts == 1 ) {
798             @parts = $self->_templates->{ $parts[0] }
799 853 100       5080 ? $self->_templates->{ $parts[0] }
    100          
800             : $parts[0] eq 'layout.html'
801             ? ( 'layout', 'default.html' )
802             : ( $self->template_dir, @parts );
803             }
804              
805             # If the default layout doesn't exist, use the old default.
806             # Remove this in v2.0
807 853 100 66     7731 if ( $parts[0] eq 'layout' && $parts[1] eq 'default.html'
      66        
      66        
808             && !$self->theme->store->path->child( @parts )->is_file
809             && $self->theme->store->path->child( site => 'layout.html.ep' )->is_file
810             ) {
811 2         179 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.};
812 2         11 return $self->theme->template( qw( site layout.html ) );
813             }
814              
815 851         74398 return $self->theme->template( @parts );
816             }
817              
818             1;
819              
820             __END__
821              
822             =pod
823              
824             =encoding UTF-8
825              
826             =head1 NAME
827              
828             Statocles::Site - An entire, configured website
829              
830             =head1 VERSION
831              
832             version 0.085
833              
834             =head1 SYNOPSIS
835              
836             my $site = Statocles::Site->new(
837             title => 'My Site',
838             nav => [
839             { title => 'Home', href => '/' },
840             { title => 'Blog', href => '/blog' },
841             ],
842             apps => {
843             blog => Statocles::App::Blog->new( ... ),
844             },
845             );
846              
847             $site->deploy;
848              
849             =head1 DESCRIPTION
850              
851             A Statocles::Site is a collection of L<applications|Statocles::App>.
852              
853             =head1 ATTRIBUTES
854              
855             =head2 title
856              
857             The site title, used in templates.
858              
859             =head2 author
860              
861             author: Doug Bell <doug@example.com>
862             author:
863             name: Doug Bell
864             email: doug@example.com
865              
866             The primary author of the site, which will be used as the default author
867             for all content. This can be a string with the author's name, and an
868             optional e-mail address wrapped in E<lt>E<gt>, or a hashref of
869             L<Statocles::Person attributes|Statocles::Person/ATTRIBUTES>.
870              
871             Individual documents can have their own authors. See
872             L<Statocles::Document/author>.
873              
874             =head2 base_url
875              
876             The base URL of the site, including protocol and domain. Used mostly for feeds.
877              
878             This can be overridden by L<base_url in Deploy|Statocles::Deploy/base_url>.
879              
880             =head2 theme
881              
882             The L<theme|Statocles::Theme> for this site. All apps share the same theme.
883              
884             =head2 apps
885              
886             The applications in this site. Each application has a name
887             that can be used later.
888              
889             =head2 plugins
890              
891             The plugins in this site. Each plugin has a name that can be used later.
892              
893             =head2 index
894              
895             The page path to use for the site index. Make sure to include the leading slash
896             (but C</index.html> is optional). Defaults to C</>, so any app with C<url_root>
897             of C</> will be the index.
898              
899             =head2 nav
900              
901             Named navigation lists. A hash of arrays of hashes with the following keys:
902              
903             title - The title of the link
904             href - The href of the link
905              
906             The most likely name for your navigation will be C<main>. Navigation names
907             are defined by your L<theme|Statocles::Theme>. For example:
908              
909             {
910             main => [
911             {
912             title => 'Blog',
913             href => '/blog',
914             },
915             {
916             title => 'Contact',
917             href => '/contact.html',
918             },
919             ],
920             }
921              
922             =head2 links
923              
924             # site.yml
925             links:
926             stylesheet:
927             - href: /theme/css/site.css
928             script:
929             - href: /theme/js/site.js
930              
931             Related links for this site. Links are used to build relationships
932             to other web addresses. Link categories are named based on their
933             relationship. Some possible categories are:
934              
935             =over 4
936              
937             =item stylesheet
938              
939             Additional stylesheets for this site.
940              
941             =item script
942              
943             Additional scripts for this site.
944              
945             =back
946              
947             Each category contains an arrayref of hashrefs of L<link objects|Statocles::Link>.
948             See the L<Statocles::Link|Statocles::Link> documentation for a full list of
949             supported attributes. The most common attributes are:
950              
951             =over 4
952              
953             =item href
954              
955             The URL for the link.
956              
957             =item text
958              
959             The text of the link. Not needed for stylesheet or script links.
960              
961             =back
962              
963             =head2 images
964              
965             # site.yml
966             images:
967             icon: /images/icon.png
968              
969             Related images for this document. These are used by themes to display
970             images in appropriate templates. Each image has a category, like
971             C<title>, C<banner>, or C<icon>, mapped to an L<image
972             object|Statocles::Image>. See the L<Statocles::Image|Statocles::Image>
973             documentation for a full list of supported attributes. The most common
974             attributes are:
975              
976             =over 4
977              
978             =item src
979              
980             The source path of the image. Relative paths will be resolved relative
981             to this document.
982              
983             =item alt
984              
985             The alternative text to display if the image cannot be downloaded or
986             rendered. Also the text to use for non-visual media.
987              
988             =back
989              
990             Useful image names are:
991              
992             =over 4
993              
994             =item icon
995              
996             The shortcut icon for the site.
997              
998             =back
999              
1000             =head2 templates
1001              
1002             # site.yml
1003             templates:
1004             sitemap.xml: custom/sitemap.xml
1005             layout.html: custom/layout.html
1006              
1007             The custom templates to use for the site meta-template like
1008             C<sitemap.xml> and C<robots.txt>, or the site-wide default layout
1009             template. A mapping of template names to template paths (relative to the
1010             theme root directory).
1011              
1012             Developers should get site templates using L<the C<template>
1013             method|/template>.
1014              
1015             =head2 template_dir
1016              
1017             The directory (inside the theme directory) to use for the site meta-templates.
1018              
1019             =head2 build_store
1020              
1021             The L<store|Statocles::Store> object to use for C<build()>. This is a workspace
1022             and will be rebuilt often, using the C<build> and C<daemon> commands. This is
1023             also the store the C<daemon> command reads to serve the site.
1024              
1025             =head2 deploy
1026              
1027             The L<deploy object|Statocles::Deploy> to use for C<deploy()>. This is
1028             intended to be the production deployment of the site. A build gets promoted to
1029             production by using the C<deploy> command.
1030              
1031             =head2 data
1032              
1033             A hash of arbitrary data available to theme templates. This is a good place to
1034             put extra structured data like social network links or make easy customizations
1035             to themes like header image URLs.
1036              
1037             =head2 log
1038              
1039             A L<Mojo::Log> object to write logs to. Defaults to STDERR.
1040              
1041             =head2 markdown
1042              
1043             The Text::Markdown object to use to turn Markdown into HTML. Defaults to a
1044             plain Text::Markdown object.
1045              
1046             Any object with a "markdown" method will work here.
1047              
1048             =head2 pages
1049              
1050             A cache of all the pages that the site contains. This is generated
1051             during the C<build> phase and is available to all the templates
1052             while they are being rendered.
1053              
1054             =head1 METHODS
1055              
1056             =head2 BUILD
1057              
1058             Register this site as the global site.
1059              
1060             =head2 app
1061              
1062             my $app = $site->app( $name );
1063              
1064             Get the app with the given C<name>.
1065              
1066             =head2 nav
1067              
1068             my @links = $site->nav( $key );
1069              
1070             Get the list of links for the given nav C<key>. Each link is a
1071             L<Statocles::Link> object.
1072              
1073             title - The title of the link
1074             href - The href of the link
1075              
1076             If the named nav does not exist, returns an empty list.
1077              
1078             =head2 build
1079              
1080             $site->build( %options );
1081              
1082             Build the site in its build location. The C<%options> hash is passed in to every
1083             app's C<pages> method, allowing for customization of app behavior based on
1084             command-line.
1085              
1086             =head2 deploy
1087              
1088             $site->deploy( %options );
1089              
1090             Deploy the site to its destination. The C<%options> are passed to the appropriate
1091             L<deploy object|Statocles::Deploy>.
1092              
1093             =head2 links
1094              
1095             my @links = $site->links( $key );
1096             my $link = $site->links( $key );
1097             $site->links( $key => $add_link );
1098              
1099             Get or append to the links set for the given key. See L<the links
1100             attribute|/links> for some commonly-used keys.
1101              
1102             If only one argument is given, returns a list of L<link
1103             objects|Statocles::Link>. In scalar context, returns the first link in
1104             the list.
1105              
1106             If two arguments are given, append the new link to the given key.
1107             C<$add_link> may be a URL string, a hash reference of L<link
1108             attributes|Statocles::Link/ATTRIBUTES>, or a L<Statocles::Link
1109             object|Statocles::Link>. When adding links, nothing is returned.
1110              
1111             =head2 url
1112              
1113             my $url = $site->url( $page_url );
1114              
1115             Get the full URL to the given path by prepending the C<base_url>.
1116              
1117             =head2 template
1118              
1119             my $template = $app->template( $tmpl_name );
1120              
1121             Get a L<template object|Statocles::Template> for the given template
1122             name. The default template is determined by the app's class name and the
1123             template name passed in.
1124              
1125             Applications should list the templates they have and describe what L<page
1126             class|Statocles::Page> they use.
1127              
1128             =head1 EVENTS
1129              
1130             The site object exposes the following events.
1131              
1132             =head2 collect_pages
1133              
1134             This event is fired after all the pages have been collected, but before they
1135             have been rendered. This allows you to edit the page's data or add/remove
1136             pages from the list.
1137              
1138             The event will be a
1139             L<Statocles::Event::Pages|Statocles::Event/Statocles::Event::Pages> object
1140             containing all the pages built by the apps.
1141              
1142             =head2 before_build_write
1143              
1144             This event is fired after the pages have been built by the apps, but before
1145             any page is written to the C<build_store>.
1146              
1147             The event will be a
1148             L<Statocles::Event::Pages|Statocles::Event/Statocles::Event::Pages> object
1149             containing all the pages built by the apps.
1150              
1151             =head2 build
1152              
1153             This event is fired after the site has been built and the pages written to the
1154             C<build_store>.
1155              
1156             The event will be a
1157             L<Statocles::Event::Pages|Statocles::Event/Statocles::Event::Pages> object
1158             containing all the pages built by the site.
1159              
1160             =head1 AUTHOR
1161              
1162             Doug Bell <preaction@cpan.org>
1163              
1164             =head1 COPYRIGHT AND LICENSE
1165              
1166             This software is copyright (c) 2016 by Doug Bell.
1167              
1168             This is free software; you can redistribute it and/or modify it under
1169             the same terms as the Perl 5 programming language system itself.
1170              
1171             =cut