Handling URL Path Prefixes

The Problem

Suppose you want your site URLs to be prefixed with the language the pages are delivered in, but you don't necessarily want to set up all your controllers to be aware of that prefix (via chaining or some other mechanism).

For example, you might want URL paths that look like "/en/user/view" and "/fr/user/view".

A Solution

At the beginning of the request cycle, the engine uses $c->prepare_path to handle the incoming request URI path.   In MyApp.pm, you can subclass $c->prepare_path to modify the way the path processing is done.

Dispatching to controller actions is based on $c->request->path, so we will need to strip the language prefix from that path.   The trick is to keep $c->request->uri, $c->request->base, and $c->request->path consistent with each other.   For example, if we originally have:

$c->request->uri  :  http://localhost:3000/en/user/view
$c->request->base :  http://localhost:3000/
$c->request->path :                        en/user/view

we will want to end up with:

$c->request->uri  :  http://localhost:3000/en/user/view
$c->request->base :  http://localhost:3000/en/
$c->request->path :                           user/view

In other words, we always want:

$c->request->uri  eq  $c->request->base . $c->request->path

Here is an example:

package MyApp;

use Catalyst::Runtime '5.70';
use base 'Catalyst';
use Catalyst qw/-Debug/;

__PACKAGE__->setup();

# This could be pulled in from config or database:
#
my %valid_languages = map { $_ => 1 } qw(en fr es);

sub prepare_path
{
    my $c = shift;
    $c->NEXT::prepare_path(@_);

    my @path_chunks = split m[/], $c->request->path, -1;

    # Ignore paths that don't start with a valid language:
    return unless @path_chunks && $valid_languages{$path_chunks[0]};

    _dump_paths($c) if $c->debug;

    # Found a language, pull it off the beginning of the path:
    my $language = shift @path_chunks;

    # Create modified request path from any remaining path chunks:
    my $path = join('/', @path_chunks) || '/';

    # Stuff modified request path back into request:
    $c->request->path($path);

    # Modify the path part of the request base
    # to include the path prefix:
    my $base = $c->request->base;
    $base->path($base->path . "$language/");

    _dump_paths($c) if $c->debug;

    $c->stash->{language} = $language;    # save language for later

    return;
}

sub _dump_paths
{
    my ($c) = @_;

    my $indent = '.' x length($c->request->base);
    $c->log->debug('Paths:',
                   "\t\$c->request->uri:  " . $c->request->uri,
                   "\t\$c->request->base: " . $c->request->base,
                   "\t\$c->request->path: $indent" . $c->request->path
                  );
}

A nice side effect is that tweaking $c->request->base to include the language means that the language will be automatically included when you call $c->uri_for later on.

In the above example, request paths that do not begin with a valid language are passed through unchanged.   But suppose for sake of consistency you'd like to make those requests look as if they had a default language prefix.   In that case, if we originally have:

$c->request->uri  :  http://localhost:3000/user/view
$c->request->base :  http://localhost:3000/
$c->request->path :                        user/view

we might want to end up with:

$c->request->uri  :  http://localhost:3000/en/user/view
$c->request->base :  http://localhost:3000/en/
$c->request->path :                           user/view

Note that in this situation we need to modify $c->request->uri instead of $c->request->path:

sub prepare_path
{
    my $c = shift;
    $c->NEXT::prepare_path(@_);

    my $language = 'en';    # default

    my @path_chunks = split m[/], $c->request->path, -1;

    if (@path_chunks && $valid_languages{$path_chunks[0]})
    {
        # Found a language, pull it off the beginning of the path:
        $language = shift @path_chunks;

        # Create modified request path from any remaining path chunks:
        my $path = join('/', @path_chunks) || '/';

        # Stuff modified request path back into request:
        $c->request->path($path);
    }
    else
    {
        # Modify the path part of the URI to look as if it had a language prefix:
        $c->request->uri->path("$language/" . $c->request->path);
    }

    # Modify the path part of the request base
    # to include the path prefix:
    my $base = $c->request->base;
    $base->path($base->path . "$language/");

    $c->stash->{language} = $language;    # save language for later

    return;
}
My tags:
 
Popular tags:
  path prefix prepare_path
Powered by Catalyst
Powered by MojoMojo Hosted by Shadowcat - Managed by Nordaaker