File Coverage

blib/lib/Statocles/Site.pm
Criterion Covered Total %
statement 168 172 97.6
branch 49 54 90.7
condition 25 32 78.1
subroutine 19 20 95.0
pod 8 8 100.0
total 269 286 94.0


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