The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Mojolicious::Plugin::CanonicalURL - Ensures canonical URLs via redirection

STATUS

Coverage Status

SYNOPSIS

# Redirects all URLs that have a slash to their no-slash equivalent with a 301 status code.
$app->plugin('CanonicalURL');

# Redirects all requests whose paths have no slash to their slash equivalent with a 301 status code.
$app->plugin('CanonicalURL', { end_with_slash => 1 });

# Canonicalize any requests whose paths match the regex qr/foo/.
$app->plugin('CanonicalURL', { should_canonicalize_request => qr/foo/ });

# Canonicalize only requests with path /foo EXACTLY.
$app->plugin('CanonicalURL', { should_canonicalize_request => '/foo' });

# Same as above, but using a subroutine. $_ contains the Mojolicious::Controller for the request.
use Mojolicious::Plugin::CanonicalURL 'remove_trailing_slashes';
$app->plugin('CanonicalURL', { should_canonicalize_request => sub { remove_trailing_slashes($_->req->url->path) eq '/foo' } });

# Same as above, but faster. Code is inlined into the subroutine. This is equally as fast as "should_canonicalize_request => '/foo'" above
# 'return $next->() unless ' added to the beginning of the string, and ';' added to the end automatically.
$app->plugin('CanonicalURL', { should_canonicalize_request => \q{remove_trailing_slashes($c->req->url->path) eq '/foo'} } });

# Same as above, with explicit 'return $next->() unless' at the beginning and ';' at the end.
$app->plugin('CanonicalURL', { should_canonicalize_request => \q{return $next->() unless remove_trailing_slashes($c->req->url->path) eq '/foo';} } });

# for multiline code to be inlined, inline_code is recommended instead
# of should_canonicalize_request
$app->plugin('CanonicalURL', {
  inline_code => q{
    my $path_no_slashes = remove_trailing_slashes($c->req->url->path);
    return $next->() if $path_no_slashes eq $path1;
    return $next->() if $path_no_slashes eq $path2;
    return $next->() if $path_no_slashes eq $path3;
  },
  # you may pass your own variables for use in inline_code
  captures => {
    '$path1' => \$path1,
    '$path2' => \$path2,
    '$path3' => \$path3,
  },
});

# canoncalize all requests that start with /foo
$app->plugin('CanonicalURL', { should_canonicalize_request => qr{^/foo} });

# Same as above, but faster because it uses index()
$app->plugin('CanonicalURL', { should_canonicalize_request => {starts_with => '/foo'} });

# canoncalize all requests that start with /foo or /bar
$app->plugin('CanonicalURL', { should_canonicalize_request => [qr{^/foo}, qr{^/bar}] });

# Same as above, but faster than qr{^/foo} or qr{^/bar} because it uses index()
$app->plugin('CanonicalURL', { should_canonicalize_request => [{starts_with => '/foo'}, {starts_with => '/bar'}] });

# Canonicalize all requests except the one with the path /foo
$app->plugin('CanonicalURL', { should_not_canonicalize_request => '/foo' });

# All options available to should_canonicalize_request are available to should_not_canonicalize_request.
# Canonicalize all requests except the one with the path /foo, any request matching qr/bar/, any request
# starting with /baz, any request with the path /qux/, or any request with the host example.com
$app->plugin('CanonicalURL', { should_not_canonicalize_request => [
    '/foo',
    qr/bar/,
    {starts_with => '/baz'}
    sub { $_->req->url->path eq '/qux/' },
    \q{$c->req->url->to_abs->host eq 'example.com'},
  ],
});

# should_canonicalize_request and should_not_canonicalize_request can be used together
# All request must start with /foo and must NOT match qr/bar/ to be canonicalized
# /foo/baz matches
# /foo/bar does not match
$app->plugin('CanonicalURL', {
    should_canonicalize_request => {starts_with => '/foo'},
    should_not_canonicalize_request => qr/bar/,
});

DESCRIPTION

Mojolicious::Plugin::CanonicalURL is a flexible and fast Mojlicious::Plugin to give you control over canonicalizing your URLs. Mojolicious::Plugin::CanonicalURL uses Sub::Quote to build the subroutine used as an "around_action" in Mojolicious hook based on the "OPTIONS" you pass in to make it as fast as possible. Mojolicious::Plugin::CanonicalURL by default redirects URLs ending with a slash in their path to their non-slash equivalent. All redirected URLs will have a status code of 301.

"end_with_slash" can be set to 1 to instead require that canonicalized URLs end with a slash.

"should_canonicalize_request" and/or "should_not_canonicalize_request" can be set to override the default that all URLs will be canonicalized.

When redirecting to the canonicalized path, all other attributes of the Mojo::URL for the request will remain the same (such as query parameters); only "path" in Mojo::URL will change to the canonicalized form.

Mojolicious::Plugin::CanonicalURL will remove multiple trailing slashes and replace them with one slash if "end_with_slash" is a true value, or no slashes if "end_with_slash" is a false value.

By default, Mojolicious::Plugin::CanonicalURL only works for dynamic actions that have methods that are called. This means that, by default, this plugin will not work for Mojolicious::Lite routes like this:

get '/' => {text => 'I ♥ Mojolicious!'};

Or routes in a full app like this:

sub startup {
  my ($self) = @_;

  my $routes = $self->routes;
  $routes->get('/' => {text => 'I ♥ Mojolicious!'});
}

See "canonicalize_before_render" to canonicalize non-dynamic actions.

OPTIONS

end_with_slash

# Require all canonicalized URLs do not end with a slash. This is the default.
$app->plugin('CanonicalURL', { end_with_slash => undef });

# Require all canonicalized URLs end with a slash.
$app->plugin('CanonicalURL', { end_with_slash => 1 });

Sets whether canonicalized URLs should end with a slash or not. Default is undef (canonicalzed URLs will not end with a slash). This matches up with how Mojolicious generates "Named-routes" in Mojolicious::Guides::Routing, which have no slash at the end. Make sure that if you set end_with_slash to 1 and you used named routes that you keep this in mind so that you do not redirect anytime a named route URL is used. A role using "after-methods" in Class::Method::Modifiers may be the correct way to add trailing slashes by setting the "trailing_slash" in Mojo::Path to 1 on the Mojo::URL.

If "end_with_slash" is false, an exception is made for the root path, /, according to RFC 2616:

Note that the absolute path cannot be empty; if none is present in the original URI, it MUST be given as "/" (the server root).

Mojolicious::Plugin::CanonicalURL will replace multiple trailing slashes with one if "end_with_slash" is true, or zero if "end_with_slash" is false.

should_canonicalize_request

"should_canonicalize_request" is responsible for determining if a given request should be canonicalized or not. "should_canonicalize_request" can be several different types of values.

"should_canonicalize_request" may be combined with "should_not_canonicalize_request" to specify conditions that must be met and conditions that must not be met for a request to be canonicalized.

SCALAR

If should_canonicalize_request is passed a scalar value, it will only canonicalize a reqeust if its path ("path" in Mojo::URL) matches the provided path. Note that all paths are compared without any trailing slashes, regardless of the value of "end_with_slash". All scalar values should be valid requests paths that start with a /.

# will only canonicalize paths where $c->req->url->path eq '/foo'
$app->plugin('CanonicalURL', { should_canonicalize_request => '/foo' });

REGEXP

If "should_canonicalize_request" is passed regex, the regex will be compared to the path of the request. If the regex does not match against the path, the request will not be canonicalized.

# $c->req->url->path =~ qr/foo/ must be true for a request to be canonicalized
$app->plugin('CanonicalURL', { should_canonicalize_request => qr/foo/ });

CODE

If "should_canonicalize_request" is passed a subroutine, $_ will contain the Mojolicious::Controller for the current request, and the truth value returned by the subroutine will determine if the request is canonicalized. If a true value is returned, the request will be canonicalized. If a false value is returned, the request will not be canonicalized.

# will only canonicalize request if path eq '/foo'
$app->plugin('CanonicalURL', {
  should_canonicalize_request => sub {
    return remove_trailing_slashes($_->req->url->path) ne '/foo';
  },
});

SCALAR REFERENCE

"SCALAR REFERENCE" is similar to "CODE", except that the code will be inlined into the generated method, avoiding the extra subroutine call.

# 'return $next->() unless ' is added if there is no return at the beginning, and ';' added at the end if it does not exist
$app->plugin('CanonicalURL', {
  should_canonicalize_request => \q{remove_trailing_slashes($c->req->url->path) eq '/foo'},
});

$next and $c from "around_action" in Mojolicious are available. "return $next->() unless " is added if the string does not begin with return, and ; is added to the end if it does not exist. If manually returning without performing canonicalization, make sure to return "$next->()".

return $next->();

You also can have your own variables through "captures":

my $my_path = '/foo';
$app->plugin('CanonicalURL', {
  captures => { '$my_path' => \$my_path },
  should_canonicalize_request => \q{remove_trailing_slashes($c->req->url->path) eq $my_path},
});

But be careful not to use any "RESERVED VARIABLE NAMES".

This is really meant for small one-liners that evaluate to a true/false value. For longer, more custom code, see "inline_code".

When comparing paths manually, be sure to handle the case where a path may or may not have a slash at the end. Mojolicious::Plugin::CanonicalURL provides "remove_trailing_slashes", which can be used in inlined code or exported.

HASH

"should_canonicalize_request" may be provided a hash reference to specify what path a request must start with to be canonicalized. The key must be starts_with, and the value should be a valid path starting with a slash.

# all reqeusts must start with '/foo' to be canonicalized
$app->plugin('CanonicalURL', { should_canonicalize_request => { starts_with => '/foo' } });

In the future, this may allow other keys, like contains, which could be a faster form of something like qr/bar/.

ARRAY

"should_canonicalize_request" can be passed an array reference with any number of elements as defined by "SCALAR", "REGEXP", "CODE", "SCALAR REFERENCE" and "HASH", and these conditions will be or'ed together, so that a request will be canonicalized if any of the conditions is true. Note that a SCALAR REFERENCE should just be a condition, without a return or trailing ;.

"captures" may be used if a scalar reference is provided in the array.

# Canonicalize any request whose path equals '/foo', any request whose path matches qr/bar/, any request
# whose path starts with '/baz', any request with the path '/qux/', or any request that has the host example.com
$app->plugin('CanonicalURL', { should_canonicalize_request => [
    '/foo',
    qr/bar/,
    {starts_with => '/baz'},
    sub { $_->req->url->path eq '/qux/' },
    \q{$c->req->url->to_abs->host eq 'example.com'},
  ],
});

should_not_canonicalize_request

"should_not_canonicalize_request" is responsible for determining if a given request should be canonicalized or not. "should_not_canonicalize_request" accepts the same types of values as "should_canonicalize_request", but handles them oppositely.

"should_not_canonicalize_request" may be combined with "should_canonicalize_request" to specify conditions that must not be met and conditions that must be met for a request to be canonicalized.

SCALAR

If should_not_canonicalize_request is passed a scalar value, it will only canonicalize a reqeust if its path ("path" in Mojo::URL) does not match the provided path. Note that all paths are compared without any trailing slashes, regardless of the value of "end_with_slash". All scalar values should be valid requests paths that start with a /.

# will only canonicalize paths where $c->req->url->path ne '/foo'
$app->plugin('CanonicalURL', { should_not_canonicalize_request => '/foo' });

REGEXP

If "should_not_canonicalize_request" is passed regex, the regex will be compared to the path of the request. If the regex matches against the path, the request will not be canonicalized.

# $c->req->url->path !~ qr/foo/ must be true for a request to be canonicalized
$app->plugin('CanonicalURL', { should_not_canonicalize_request => qr/foo/ });

CODE

If "should_not_canonicalize_request" is passed a subroutine, $_ will contain the Mojolicious::Controller for the current request, and the truth value returned by the subroutine will determine if the request is canonicalized. If a true value is returned, the request will not be canonicalized. If a false value is returned, the request will be canonicalized.

# will not canonicalize request if path eq '/foo'
$app->plugin('CanonicalURL', {
  should_not_canonicalize_request => sub {
    return remove_trailing_slashes($_->req->url->path) eq '/foo';
  },
});

SCALAR REFERENCE

SCALAR REFERECE is similar to CODE, except that the code will be inlined into the generated method, avoiding the extra subroutine call.

# 'return $next->() if ' is added if there is no return at the beginning, and ';' added at the end if it does not exist
$app->plugin('CanonicalURL', {
  should_not_canonicalize_request => \q{remove_trailing_slashes($c->req->url->path) eq '/foo'},
});

$next and $c from "around_action" in Mojolicious are available. "return $next->() if " is added if the string does not begin with return, and ; is added to the end if it does not exist. If manually returning without performing canonicalization, make sure to return "$next->()".

return $next->();

You also can have your own variables through "captures":

my $my_path = '/foo';
$app->plugin('CanonicalURL', {
  captures => { '$my_path' => \$my_path },
  should_not_canonicalize_request => \q{remove_trailing_slashes($c->req->url->path) eq $my_path},
});

But be careful not to use any "RESERVED VARIABLE NAMES".

This is really meant for small one-liners that evaluate to a true/false value. For longer, more custom code, see "inline_code".

When comparing paths manually, be sure to handle the case where a path may or may not have a slash at the end. Mojolicious::Plugin::CanonicalURL provides "remove_trailing_slashes", which can be used in inlined code or exported.

HASH

"should_not_canonicalize_request" may be provided a hash reference to specify what paths a request must start with to not be canonicalized. The key must be starts_with, and the value should be a valid path starting with a slash.

# all reqeusts must not start with '/foo' to be canonicalized
$app->plugin('CanonicalURL', { should_not_canonicalize_request => { starts_with => '/foo' } });

In the future, this may allow other keys, like contains, which could be a faster form of something like qr/bar/.

ARRAY

"should_not_canonicalize_request" can be passed an array reference with any number of elements as defined by SCALAR, REGEXP, CODE, SCALAR REFERENCE and HASH, and these conditions will be or'ed together, so that a request will not be canonicalized if any of the conditions is true. Note that a SCALAR REFERENCE should just be a condition, without a return or trailing ;.

"captures" may be used if a scalar reference is provided in the array.

# Do not canonicalize any request whose path equals '/foo', any request whose path matches qr/bar/, any request
# whose path starts with '/baz', any request with the path '/qux/', or any request that has the host example.com
$app->plugin('CanonicalURL', { should_not_canonicalize_request => [
    '/foo',
    qr/bar/,
    {starts_with => '/baz'},
    sub { $_->req->url->path eq '/qux/' },
    \q{$c->req->url->to_abs->host eq 'example.com'},
  ],
});

inline_code

"inline_code" allows you to pass in code that will be run right before the code to canonicalize a request runs, but after any code generated by "should_canonicalize_request" or "should_not_canonicalize_request". This is meant for larger, more custom code than the scalar inline code allowed for "should_canonicalize_request" and "should_not_canonicalize_request".

$app->plugin('CanonicalURL', {
  inline_code => q{
    # custom code
    if (...) {
      $c->app->log("Path is " . $c->req->url->path);
      return $next->() if remove_trailing_slashes($c->req->url->path) eq '/foo';
    }
  },
});

If "inline_code" is set, "captures" may be used to access your variables inside of the code:

$app->plugin('CanonicalURL', {
  captures => { '$my_var' => \$my_var },
  inline_code => q{
    if (...) {
      # custom code
      $c->app->log("My var is $my_var");
      return $next->() if $c->req->url->path eq $my_var;
    }
  },
});

When comparing paths manually, be sure to handle the case where a path may or may not have a slash at the end. Mojolicious::Plugin::CanonicalURL provides "remove_trailing_slashes", which can be used in inlined code or exported.

captures

my $skip = {
  '/path_one' => 1,
  '/path_two' => 1,
};

$app->plugin('CanonicalURL', {
  captures => { '$skip' => \$skip },
  should_canonicalize_request => \'exists $skip->{remove_trailing_slashes($c->req->url->path)}',
});

# same as above using inline_code
$app->plugin('CanonicalURL', {
  captures => { '$skip' => \$skip },
  inline_code => q{
    if (exists $skip->{$c->req->url->path}) {
      return $next->();
    }
  },
});

"captures" corresponds to captures in Sub::Quote:

\%captures is a hashref of variables that will be made available to the code.
The keys should be the full name of the variable to be made available, including the sigil.
The values should be references to the values. The variables will contain copies of the values.

"captures" can only be used when "should_canonicalize_request" or "should_not_canonicalize_request" use "SCALAR REFERENCE" or SCALAR REFERENCE, or when a scalar reference is provided in "ARRAY" or ARRAY for "should_canonicalize_request" or "should_not_canonicalize_request", or when "inline_code" is set.

See "SYNOPSIS" in Sub::Quote's Silly::dragon for an example using captures.

canonicalize_before_render

# Now the before_render hook will be used to canonicalize requests in addition to the around_action hook.
$app->plugin('CanonicalURL', {
  canonicalize_before_render => 1,
});

# useful for actions defined like this in Mojolicious::Lite which will not trigger the around_action hook
get '/' => {text => 'I ♥ Mojolicious!'};

# or actions like this in the full app
sub startup {
  my ($self) = @_;

  my $routes = $self->routes;
  $routes->get('/' => {text => 'I ♥ Mojolicious!'});
}

For text routes as shown above, no action is actually called, because the text is stored with the route and rendered from the string without being wrapped in a subroutine. This means that requests for these routes cannot be canonicalized using the "around_action" in Mojolicious hook. When using routes of this format, "canonicalize_before_render" must be set to 1 so that the "before_render" in Mojolicious hook will be used instead to canonicalize these requests. If a request was already successfully canonicalized (or redirected by other means) in the "around_action" in Mojolicious hook, then the "before_render" in Mojolicious hook will return early. However, both the "around_action" in Mojolicious and "before_render" in Mojolicious hooks will be called when a request does not need to be canonicalized, performing the same work twice (the performance of this shouldn't be a problem, especially for lite apps).

By default, "canonicalize_before_render" is undef, meaning that the "before_render" in Mojolicious hook is not used to canonicalize requests, only the "around_action" in Mojolicious hook is used.

EXPORTED

remove_trailing_slashes

Removes all trailing slashes from a path. Accepts a string or a blessed reference that overloads "", such as Mojo::Path. This is useful for examining paths.

# export to use in your code
use Mojolicious::Plugin::CanonicalURL 'remove_trailing_slashes';
$app->plugin('CanonicalURL', { should_canonicalize_request => sub { remove_trailing_slashes($_->req->url->path) eq '/foo' } });

# or use in code that is inlined
$app->plugin('CanonicalURL', { should_canonicalize_request => \q{remove_trailing_slashes($_->req->url->path) eq '/foo'} });

"remove_trailing_slashes" can be exported or used by inlined code, such as when "should_canonicalize_request" or "should_not_canonicalize_request" use "SCALAR REFERENCE" or SCALAR REFERENCE, or when "inline_code" is set.

RESERVED VARIABLE NAMES

These are the variable names that Mojolicious::Plugin::CanonicalURL uses and you should avoid using when declaring your own variables via "inline_code", "SCALAR REFERENCE", SCALAR REFERENCE, or "captures":

  • $c

  • $next

  • $_mpcu_path

  • $_mpcu_path_length

  • $_mpcu_path_with_no_slashes_at_the_end

  • $_mpcu_*

AUTHOR

Adam Hopkins <srchulo@cpan.org>

COPYRIGHT

Copyright 2019- Adam Hopkins

LICENSE

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

SEE ALSO