File Coverage

blib/lib/Search/Sitemap.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Search::Sitemap;
2             $Search::Sitemap::VERSION = '2.13_01';
3 1     1   584 use 5.008003;
  1         3  
  1         30  
4 1     1   5 use strict;
  1         1  
  1         36  
5 1     1   4 use warnings;
  1         1  
  1         35  
6             our $AUTHORITY = 'cpan:JASONK';
7 1     1   219 use Moose;
  0            
  0            
8             use Search::Sitemap::Types qw(
9             SitemapUrlStore XMLPrettyPrintValue SitemapURL
10             );
11             use MooseX::ClassAttribute;
12             use MooseX::Types::Moose qw( Str HashRef Bool ArrayRef CodeRef );
13             use Search::Sitemap::URL;
14             use XML::Twig;
15             use IO::File;
16             use Carp qw( carp croak );
17             use HTML::Entities qw( decode_entities );
18             use Class::Load;
19             use namespace::clean -except => 'meta';
20              
21             has 'urls' => (
22             is => 'ro',
23             isa => SitemapUrlStore,
24             coerce => 1,
25             default => sub {
26             Class::Load::load_class( 'Search::Sitemap::URLStore::Memory' );
27             return Search::Sitemap::URLStore::Memory->new;
28             },
29             handles => {
30             get_url => 'get',
31             put_url => 'put',
32             put_urls => 'put',
33             find_url => 'find',
34             },
35             );
36              
37             has 'pretty' => (
38             is => 'rw',
39             isa => XMLPrettyPrintValue,
40             coerce => 1,
41             default => 'none',
42             );
43              
44             class_has 'base_element' => (
45             is => 'rw',
46             isa => Str,
47             default => 'urlset'
48             );
49              
50             has 'xmlparser' => (
51             is => 'rw',
52             isa => 'XML::Twig',
53             lazy => 1,
54             default => sub {
55             my $self = shift;
56              
57             XML::Twig->new(
58             twig_roots => {
59             $self->base_element => sub {
60             my ( $twig, $elt ) = @_;
61             foreach my $c ( $elt->children ) {
62             my %url = ();
63             my $var = $c->gi;
64             croak "Unrecognised element $var"
65             unless $var =~ /^(?:url|sitemap)$/;
66             foreach my $e ( $c->children ) {
67             $url{ $e->gi } = decode_entities( $e->text );
68             }
69             $self->update( \%url );
70             }
71             $twig->purge;
72             },
73             },
74             );
75             },
76             handles => [qw( safe_parse )],
77             );
78              
79             has 'have_zlib' => (
80             is => 'ro',
81             isa => Bool,
82             lazy => 1,
83             default => sub {
84             local $@;
85             eval { Class::Load::load_class( 'IO::Zlib' ) };
86             return $@ ? 0 : 1;
87             },
88             );
89              
90             has 'extensions' => ( is => 'ro', isa => HashRef, default => sub { {} } );
91              
92             has 'xml_headers' => ( is => 'rw', isa => HashRef, lazy_build => 1 );
93             sub _build_xml_headers {
94             my $self = shift;
95             my $ext = $self->extensions;
96             return {
97             'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
98             'xsi:schemaLocation' => join( ' ',
99             'http://www.sitemaps.org/schemas/sitemap/0.9',
100             'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
101             ),
102             'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
103             ( map { ( "xmlns:$_" => $ext->{ $_ } ) } keys %{ $ext } ),
104             }
105             }
106              
107             sub xml {
108             my $self = shift;
109              
110             my $xml = XML::Twig::Elt->new(
111             $self->base_element => $self->xml_headers,
112             ( map {
113             $_->as_elt( $self->url_type => $self->url_fields );
114             } $self->urls->all ),
115             );
116             $xml->set_pretty_print( $self->pretty );
117             my $header = '<?xml version="1.0" encoding="UTF-8"?>';
118             if ( $self->pretty ) { $header .= "\n" }
119             return $header.$xml->sprint();
120             }
121              
122             class_has 'url_type' => ( is => 'rw', isa => Str, default => 'url' );
123             class_has 'url_fields' => (
124             is => 'rw',
125             isa => ArrayRef[Str],
126             auto_deref => 1,
127             default => sub { [qw( loc lastmod changefreq priority )] }
128             );
129              
130             sub BUILD {
131             my ( $self, $args ) = @_;
132              
133             $self->urls->add_trigger( put => sub {
134             my $self = shift;
135             return if $self->extensions->{ 'mobile' };
136             for my $url ( @_ ) {
137             if ( $url->has_mobile && $url->mobile ) {
138             $self->extensions->{ 'mobile' } = 'http://www.google.com/schemas/sitemap-mobile/1.0';
139             return;
140             }
141             }
142             } );
143             }
144              
145             sub read {
146             my ( $self, $file ) = @_;
147              
148             croak "No filename specified for ".ref( $self )."->read" unless $file;
149              
150             # don't try to parse missing or empty files
151             # no errors for this, because we might be creating it
152             return unless -f $file && -s _;
153              
154             # don't try to parse very small compressed files
155             # (empty .gz files are 20 bytes)
156             return if $file =~ /\.gz/ && -s $file < 50;
157              
158             if ( $file =~ /\.gz$/ ) {
159             croak "IO::Zlib not available, cannot read compressed sitemaps"
160             unless $self->have_zlib;
161             $self->safe_parse( IO::Zlib->new( $file => "rb" ) );
162             } else {
163             $self->safe_parse( IO::File->new( $file => "r" ) );
164             }
165             }
166              
167             sub write {
168             my ( $self, $file ) = @_;
169              
170             croak "No filename specified for ".ref( $self )."->write" unless $file;
171              
172             my $fh;
173             if ( $file =~ /\.gz$/i ) {
174             croak "IO::Zlib not available, cannot write compressed sitemaps"
175             unless $self->have_zlib;
176             $fh = IO::Zlib->new( $file => 'wb9' );
177             } else {
178             $fh = IO::File->new( $file => 'w' );
179             }
180             croak "Could not create '$file'" unless $fh;
181             $fh->print( $self->xml );
182             }
183              
184             sub update {
185             my $self = shift;
186             my $data = ( @_ == 1 ) ? shift : { @_ };
187             my $loc = $data->{ 'loc' } or croak "Can't call ->update without 'loc'";
188             if ( my $obj = $self->get_url( $loc ) ) {
189             for my $key ( keys %{ $data } ) {
190             next if $key eq 'loc';
191             $obj->$key( $data->{ $key } );
192             }
193             return $obj;
194             } else {
195             my $obj = Search::Sitemap::URL->new( $data );
196             $self->put_url( $obj );
197             return $obj;
198             }
199             }
200              
201             sub add {
202             my $self = shift;
203              
204             my @urls = ();
205             if ( ref $_[0] ) {
206             push( @urls, map { to_SitemapURL( $_ ) } @_ );
207             } elsif ( $_[0] =~ m{://} ) {
208             push( @urls, map { Search::Sitemap::URL->new( loc => $_ ) } @_ );
209             } else {
210             push( @urls, Search::Sitemap::URL->new( @_ ) );
211             }
212             $self->put_urls( @urls );
213             return ( @urls == 1 ) ? $urls[0] : wantarray ? @urls : \@urls;
214             }
215              
216             __PACKAGE__->meta->make_immutable;
217             1;
218             __END__
219              
220             =encoding utf-8
221              
222             =head1 NAME
223              
224             Search::Sitemap - Perl extension for managing Search Engine Sitemaps
225              
226             =head1 SYNOPSIS
227              
228             use Search::Sitemap;
229            
230             my $map = Search::Sitemap->new();
231             $map->read( 'sitemap.gz' );
232            
233             # Main page, changes a lot because of the blog
234             $map->add( Search::Sitemap::URL->new(
235             loc => 'http://www.jasonkohles.com/',
236             lastmod => '2005-06-03',
237             changefreq => 'daily',
238             priority => 1.0,
239             ) );
240            
241             # Top level directories, don't change as much, and have a lower priority
242             $map->add( {
243             loc => "http://www.jasonkohles.com/$_/",
244             changefreq => 'weekly',
245             priority => 0.9, # lower priority than the home page
246             } ) for qw(
247             software gpg hamradio photos scuba snippets tools
248             );
249            
250             $map->write( 'sitemap.gz' );
251              
252             =head1 DESCRIPTION
253              
254             The Sitemap Protocol allows you to inform search engine crawlers about URLs
255             on your Web sites that are available for crawling. A Sitemap consists of a
256             list of URLs and may also contain additional information about those URLs,
257             such as when they were last modified, how frequently they change, etc.
258              
259             This module allows you to create and modify sitemaps.
260              
261             =head1 METHODS
262              
263             =head2 new()
264              
265             Creates a new Search::Sitemap object.
266              
267             my $map = Search::Sitemap->new();
268              
269             =head2 read( $file )
270              
271             Read a sitemap in to this object. Reading of compressed files is done
272             automatically if the filename ends with .gz.
273              
274             =head2 write( $file )
275              
276             Write the sitemap out to a file. Writing of compressed files is done
277             automatically if the filename ends with .gz.
278              
279             =head2 urls()
280              
281             Return the L<Search::Sitemap::URLStore> object that make up the sitemap.
282              
283             To get all urls (L<Search::Sitemap::URL> objects) please use:
284              
285             my @urls = $map->urls->all;
286              
287             =head2 add( $item, [$item...] )
288              
289             Add the L<Search::Sitemap::URL> items listed to the sitemap.
290              
291             If you pass hashrefs instead of objects, it will turn them into objects for
292             you. If the first item you pass is a simple scalar that matches \w, it will
293             assume that the values passed are a hash for a single object. If the first
294             item passed matches m{^\w+://} (i.e. it looks like a URL) then all the
295             arguments will be treated as URLs, and L<Search::Sitemap::URL> objects will be
296             constructed for them, but only the loc field will be populated.
297              
298             This means you can do any of these:
299              
300             # create the Search::Sitemap::URL object yourself
301             my $url = Search::Sitemap::URL->new(
302             loc => 'http://www.jasonkohles.com/',
303             priority => 1.0,
304             );
305             $map->add($url);
306            
307             # or
308             $map->add(
309             { loc => 'http://www.jasonkohles.com/' },
310             { loc => 'http://www.jasonkohles.com/software/search-sitemap/' },
311             { loc => 'http://www.jasonkohles.com/software/geo-shapefile/' },
312             );
313            
314             # or
315             $map->add(
316             loc => 'http://www.jasonkohles.com/',
317             priority => 1.0,
318             );
319            
320             # or even something funkier
321             $map->add( qw(
322             http://www.jasonkohles.com/
323             http://www.jasonkohles.com/software/search-sitemap/
324             http://www.jasonkohles.com/software/geo-shapefile/
325             http://www.jasonkohles.com/software/text-fakedata/
326             ) );
327             foreach my $url ( $map->urls ) { $url->changefreq( 'daily' ) }
328              
329             =head2 update
330              
331             Similar to L</add>, but while L</add> will replace an existing object that
332             has the same URL, update will update the provided values.
333              
334             As as example, if you do this:
335              
336             $map->add(
337             loc => 'http://www.example.com/',
338             priority => 1.0,
339             );
340             $map->add(
341             loc => 'http://www.example.com/',
342             changefreq => 'daily',
343             );
344              
345             The sitemap will end up containing this:
346              
347             <url>
348             <loc>http://www.example.com</loc>
349             <changefreq>daily</changefreq>
350             </url>
351              
352             But if instead you use update:
353              
354             $map->update(
355             loc => 'http://www.example.com/',
356             priority => 1.0,
357             );
358             $map->update(
359             loc => 'http://www.example.com/',
360             changefreq => 'daily',
361             );
362              
363             This sitemap will end up with this:
364              
365             <url>
366             <loc>http://www.example.com</loc>
367             <changefreq>daily</changefreq>
368             <priority>1.0</priority>
369             </url>
370              
371             =head2 xml();
372              
373             Return the xml representation of the sitemap.
374              
375             =head2 pretty()
376              
377             Set this to a true value to enable 'pretty-printing' on the XML output. If
378             false (the default) the XML will be more compact but not as easily readable
379             for humans (Google and other computers won't care what you set this to).
380              
381             If you set this to a 'word' (something that matches /[a-z]/i), then that
382             value will be passed to XML::Twig directly (see the L<XML::Twig> pretty_print
383             documentation). Otherwise if a true value is passed, it means 'nice', and a
384             false value means 'none'.
385              
386             Returns the value it was set to, or the current value if called with no
387             arguments.
388              
389             =head1 ACKNOWLEDGEMENTS
390              
391             Thanks to Alex J. G. BurzyÅ„ski for help with maintaining this module.
392              
393             =head1 SEE ALSO
394              
395             L<Search::Sitemap::Index>
396              
397             L<Search::Sitemap::Ping>
398              
399             L<Search::Sitemap::Robot>
400              
401             L<http://www.sitemaps.org/>
402              
403             =head1 AUTHOR
404              
405             Jason Kohles, E<lt>email@jasonkohles.comE<gt>
406              
407             =head1 COPYRIGHT AND LICENSE
408              
409             Copyright (C) 2005-2009 by Jason Kohles
410              
411             This library is free software; you can redistribute it and/or modify
412             it under the same terms as Perl itself.
413              
414             =cut
415