—package
Mojolicious::Plugin::Export;
our
$VERSION
=
'0.008'
;
# ABSTRACT: Export a Mojolicious website to static files
#pod =head1 SYNOPSIS
#pod
#pod use Mojolicious::Lite;
#pod get '/' => 'index';
#pod get '/secret' => 'secret';
#pod plugin Export => {
#pod pages => [qw( / /secret )],
#pod };
#pod app->start;
#pod
#pod =head1 DESCRIPTION
#pod
#pod Deploy a Mojolicious webapp to static files.
#pod
#pod =head2 Configuration
#pod
#pod Default values for the command's options can be specified in the
#pod configuration using one of Mojolicious's configuration plugins.
#pod
#pod # myapp.conf
#pod {
#pod export => {
#pod # Configure the default pages to export
#pod pages => [ '/', '/hidden' ],
#pod # The directory to export to
#pod to => '/var/www/html',
#pod # Rewrite URLs to include base directory
#pod base => '/',
#pod }
#pod }
#pod
#pod =head1 HELPERS
#pod
#pod =head2 export
#pod
#pod The C<export> helper returns the L<Mojolicious::Plugin::Export> object.
#pod
#pod =head1 EVENTS
#pod
#pod =head2 before_write
#pod
#pod Emitted after all the content has been loaded and prepared for export.
#pod The event is given two arguments: The Mojolicious::Plugin::Export object
#pod and a hashref of paths mapped to content to be exported. The content can
#pod be either a Mojo::DOM object for HTML pages or the content to be
#pod exported. This event may modify the hashref or the DOM objects to change
#pod the content before it is written.
#pod
#pod app->export->on( before_write => sub {
#pod my ( $export, $pages ) = @_;
#pod for my $path ( keys %$pages ) {
#pod my $content = $pages->{ $path };
#pod # ...
#pod }
#pod } );
#pod
#pod =head1 SEE ALSO
#pod
#pod L<Mojolicious::Command::export>, L<Mojolicious::Plugin::Export::Git>, L<Mojolicious::Plugin>
#pod
#pod =cut
#pod =attr pages
#pod
#pod The pages to export by default. This can be overridden by the arguments to
#pod L</export>.
#pod
#pod # Add pages to export by default
#pod push @{ $app->export->pages }, '/blog';
#pod
#pod =cut
has
pages
=>
sub
{ [] };
#pod =attr to
#pod
#pod The path to export to by default.
#pod
#pod =cut
has
to
=>
'.'
;
#pod =attr base
#pod
#pod The base URL, if URLs need to be rewritten.
#pod
#pod =cut
has
base
=>
''
;
#pod =attr quiet
#pod
#pod If true, will not report every action taken by the plugin. Defaults to true.
#pod
#pod =cut
has
quiet
=> 1;
has
_app
=>;
sub
register {
my
(
$self
,
$app
,
$plugin_conf
) =
@_
;
$self
->_app(
$app
);
$app
->helper(
export
=>
sub
{
$self
} );
# Config file overrides plugin config
my
$config
=
$app
->can(
'config'
) ?
$app
->config->{export} : {};
for
my
$key
(
keys
%$config
) {
if
( !
$self
->can(
$key
) ) {
die
"Unrecognized export configuration: $key\n"
;
}
$self
->
$key
(
$config
->{
$key
} //
$plugin_conf
->{
$key
} );
}
return
$self
;
}
#pod =method export
#pod
#pod app->export->export( $override );
#pod
#pod Export the site. C<$override> is a hash reference to override the object
#pod attributes (keys are attribute names, values are the overridden value).
#pod
#pod =cut
sub
export {
my
(
$self
,
$opt
) =
@_
;
for
my
$key
(
qw( pages to base quiet )
) {
$opt
->{
$key
} //=
$self
->
$key
;
}
if
(
$opt
->{base} &&
$opt
->{base} =~ m{^[^/]} ) {
$opt
->{base} =
'/'
.
$opt
->{base};
}
my
$root
= path(
$opt
->{ to } );
my
@pages
= @{
$opt
->{pages} } ?
map
{ m{^/} ?
$_
:
"/$_"
} @{
$opt
->{pages} }
: (
'/'
);
my
$ua
= Mojo::UserAgent->new;
$ua
->server->app(
$self
->_app );
# A hash of path => knowledge about the path
# link_from => a hash of path -> array of DOM elements linking to original path
# res => The response from the request for this page
# redirect_to => The redirect location, if it was a redirect
my
%history
;
while
(
my
$page
=
shift
@pages
) {
next
if
$history
{
$page
}{ res };
my
$tx
=
$ua
->get(
$page
);
my
$res
=
$tx
->res;
# Do not try to write error messages
if
(
$res
->is_error ) {
if
( !
$opt
->{quiet} ) {
say
sprintf
" [ERROR] %s - %s %s"
,
$page
,
$res
->code,
$res
->message;
}
next
;
}
# Rewrite links to redirects
if
(
$res
->is_redirect ) {
my
$loc
=
$history
{
$page
}{ redirect_to } =
$res
->headers->location;
for
my
$link_from
(
keys
%{
$history
{
$page
}{ link_from } } ) {
for
my
$el
( @{
$history
{
$page
}{ link_from }{
$link_from
} } ) {
$el
->attr(
href
=>
$loc
);
}
}
next
;
}
my
$type
=
$res
->headers->content_type;
if
(
$type
and
$type
=~ m{^text/html} and
my
$dom
=
$res
->dom ) {
my
$dir
=
$page
=~ m{/$} ? path(
$page
) : path(
$page
)->dirname;
for
my
$attr
(
qw( href src )
) {
for
my
$el
(
$dom
->find(
"[$attr]"
)->
each
) {
my
$url
=
$el
->attr(
$attr
);
# Don't analyze full URLs
next
if
$url
=~ m{^(?:[a-zA-Z]+:|//)};
# Don't analyze in-page fragments
next
if
$url
=~ m{^
#};
# Fix relative paths
my
$path
=
$url
=~ m{^/} ?
$url
:
$dir
->child(
$url
).
""
;
# Remove fragment
$path
=~ s/
#.+//;
if
(
my
$loc
=
$history
{
$path
}{ redirect_to } ) {
$el
->attr(
$attr
=>
$loc
);
next
;
}
else
{
push
@{
$history
{
$path
}{ link_from }{
$page
} },
$el
;
}
if
( !
$history
{
$path
}{ res } ) {
push
@pages
,
$path
;
}
}
}
}
$history
{
$page
}{ res } =
$res
;
}
# Event for checking the status of everything we're about to export.
# We do this before rewriting the base URLs to make it easier to
# check for broken links.
my
%to_export
=
map
{
$_
->[0] =>
$_
->[1] =~ m{^text/html} ?
$_
->[2]->dom :
$_
->[2]->body }
map
{ [
$_
,
$history
{
$_
}{ res }->headers->content_type,
$history
{
$_
}{ res } ] }
grep
{
$history
{
$_
}{ res } }
keys
%history
;
$self
->emit(
before_write
=> \
%to_export
);
for
my
$page
(
keys
%to_export
) {
my
$content
=
$to_export
{
$page
};
if
(
ref
$content
eq
'Mojo::DOM'
) {
my
$dir
= path(
$page
)->dirname;
for
my
$attr
(
qw( href src )
) {
for
my
$el
(
$content
->find(
"[$attr]"
)->
each
) {
my
$url
=
$el
->attr(
$attr
);
# Rewrite absolute paths
if
(
$opt
->{base} &&
$url
=~ m{^/} ) {
my
$base_url
=
$url
eq
'/'
?
$opt
->{base} :
$opt
->{base} .
$url
;
$el
->attr(
$attr
=>
$base_url
);
}
}
}
}
$self
->_write(
$root
,
$page
,
$content
,
$opt
->{quiet} );
}
}
sub
_write {
my
(
$self
,
$root
,
$page
,
$content
,
$quiet
) =
@_
;
if
(
ref
$content
eq
'Mojo::DOM'
) {
# Mojolicious automatically decodes using the response content
# type, so all we need to do is encode it into the file content
# type that we want
# TODO: Allow configuring the destination encoding
# TODO: Ensure all text/* MIME types use the destination
# encoding
$content
= encode
'utf8'
,
$content
;
}
my
$to
=
$root
->child(
$page
);
if
(
$to
!~ m{[.][^/.]+$} ) {
$to
=
$to
->child(
'index.html'
);
}
my
$dir
=
$to
->dirname;
if
( !-d
$dir
) {
$dir
->make_path;
say
" [mkdir] $dir"
unless
$quiet
;
}
else
{
say
" [exist] $dir"
unless
$quiet
;
}
say
" [write] $to"
unless
$quiet
;
$to
->spurt(
$content
);
}
1;
__END__
=pod
=head1 NAME
Mojolicious::Plugin::Export - Export a Mojolicious website to static files
=head1 VERSION
version 0.008
=head1 SYNOPSIS
use Mojolicious::Lite;
get '/' => 'index';
get '/secret' => 'secret';
plugin Export => {
pages => [qw( / /secret )],
};
app->start;
=head1 DESCRIPTION
Deploy a Mojolicious webapp to static files.
=head2 Configuration
Default values for the command's options can be specified in the
configuration using one of Mojolicious's configuration plugins.
# myapp.conf
{
export => {
# Configure the default pages to export
pages => [ '/', '/hidden' ],
# The directory to export to
to => '/var/www/html',
# Rewrite URLs to include base directory
base => '/',
}
}
=head1 ATTRIBUTES
=head2 pages
The pages to export by default. This can be overridden by the arguments to
L</export>.
# Add pages to export by default
push @{ $app->export->pages }, '/blog';
=head2 to
The path to export to by default.
=head2 base
The base URL, if URLs need to be rewritten.
=head2 quiet
If true, will not report every action taken by the plugin. Defaults to true.
=head1 METHODS
=head2 export
app->export->export( $override );
Export the site. C<$override> is a hash reference to override the object
attributes (keys are attribute names, values are the overridden value).
=head1 HELPERS
=head2 export
The C<export> helper returns the L<Mojolicious::Plugin::Export> object.
=head1 EVENTS
=head2 before_write
Emitted after all the content has been loaded and prepared for export.
The event is given two arguments: The Mojolicious::Plugin::Export object
and a hashref of paths mapped to content to be exported. The content can
be either a Mojo::DOM object for HTML pages or the content to be
exported. This event may modify the hashref or the DOM objects to change
the content before it is written.
app->export->on( before_write => sub {
my ( $export, $pages ) = @_;
for my $path ( keys %$pages ) {
my $content = $pages->{ $path };
# ...
}
} );
=head1 SEE ALSO
L<Mojolicious::Command::export>, L<Mojolicious::Plugin::Export::Git>, L<Mojolicious::Plugin>
=head1 AUTHOR
Doug Bell <preaction@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2018 by Doug Bell.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut