Yii 2 canonical URL in urlManager configuration
Asked Answered
C

3

9

I've got urlManager section in app configuration with several URLs per route:

    'urlManager' => [
        'enablePrettyUrl' => true,
        'showScriptName' => false,
        'enableStrictParsing' => true,
        'rules' => [
            'article-a' => 'article/a', // canonic comes first
            'article-deprecated-a' => 'article/a',
            'article-another-a-is-deprecated' => 'article/a',
            'b-annoucement' => 'announcement/b', // canonic comes first
            'legacy-b-annoncement' => 'announcement/b',
            ...

SEF URLs for routes are stored in frontend/config/main.php as an array, with multiple URLs per route. The first URL for the given route (i.e. /article-a) is canonical and the rest are legacy URLs.

What's the most natural way to specify canonical URL for a group of URLs that are pointing to the same route? It can be either rel="canonical" in view or 301/302 redirect to canonical URL.

Canonical URLs should be preferably specified in a place where the routes were defined (frontend/config/main.php configuration file in this case). The requirement here is that canonical URL should be defined outside of the controller, not hard-coded to controller.

Cusec answered 11/3, 2016 at 3:28 Comment(6)
I think you should open this up as an issue on Github. I looked through the yii\web\UrlManager and I do not see any means to define a canonical URL which groups others in rules[].Toon
@Yasky Yes, I guess it isn't an another 'rtfm' question. I'm positive that the framework is powerful enough to be able to provide this logic for rules, even if it isn't available out of the box (via events, behaviours, url rule classes, etc), just not sure how.Cusec
it may need a different class. 'urlManager'=>['class' => 'app\components\myUrlManager' that extends yii\web\UrlManager and overrides or add extra methods to itBronder
@SalemOuerdani Sounds good. Should it be an override for buildRules? And how would I force 301/302 redirect on some rules from there?Cusec
I don't know yet. the thing is I only used Yii2 to build REST apis so far so I didn't have to do similar things yet. But I just found this which may also be a good option. a custom UrlRule class that implements UrlRuleInterface. I need to read more stuffs and if I figure out a clean way to do it I'll post.Bronder
In fact Yii2 REST api itself is using yii\rest\UrlRule which extends yii\web\CompositeUrlRule, an implementation of the UrlRuleInterface. So it is a custom child UrlRule class after all. I didn't even notice that but it should be a good example to check for a start.Bronder
B
6

I'm not sure how exactly you need to manage your rules so I'll go for a general use case and I'll base my answer on what I did understand from Paddy Moogan's Article which I will resume within the following example and I hope it helps on designing your required solution:

requirement:

Assuming a Search Engine did send a robot to check page B in my website and I'm not fine with people getting to page B instead of page A. So this is how I can clarify my point to the robot:

  1. Forcing a 301 redirect to page A:

    Telling the Search Engine that this page is permanently moved to page A. So please don't send more people to it. Send them to page A instead.

  2. Forcing a 302 redirect to page A:

    Telling the Search Engine that this page is temporary moved to page A. So do whatever you think it is appropriate.

  3. Opening page B (200 status code) but insert a Canonical link element pointing to page A:

    Telling the Search Engine that this page is working fine but it is to me a secondary page and I would suggest sending the next visitors to page A instead.


design:

So based on that this is how I would see a possible structure to my rules configuration:

'rules' => [
    [
        // by default: 'class' => 'yii\web\UrlRule',
        'pattern' => '/',
        'route' => 'site/index',
    ],
    [
        // the custom class
        'class' => 'app\components\SEOUrlRule',

        'pattern' => 'about',
        'route' => 'site/about',

        'permanents' => [
            'deprecated-about',
            'an-older-deprecated-about'
        ],

        'temporaries' => [
            'under-construction-about',
        ],

        'secondaries' => [
            'about-page-2'
        ]
    ],
    [
        // different route with own action but canonical should be injected
        'class' => 'app\components\SEOUrlRule',
        'pattern' => 'experimental-about',
        'route' => 'whatever/experimental',
        'canonical' => 'about'
    ],
]

This way I can chain as much arrays as I need to use Yii's default class yii\web\UrlRule while I can have a custom one in my app components folder dedicated to SEO related controllers.

Before going to code, this is how I would expect my website to behave :

  • You visit the /about page you get a 200 response (no canonical added).
  • You visit the /deprecated-about page you get redirected to /about with 301 status code.
  • You visit the /under-construction-about page you get redirected to /about with 302 status code.
  • You visit the /about-page-2 page you get a 200 response (rendered by index/about action). No redirections except a similar tag to this is automatically injected into source code: <link href="http://my-website/about" rel="canonical">
  • You visit the /experimental-about page you get a 200 response (rendered by its own action whatever/experimental) but with that same canonical tag above injected.

code:

The SEOUrlRule will simply extend \yii\web\UrlRule and override its parseRequest method to define the extra attributes based on which we will force a HTTP redirection or call parent::parseRequest() after registering the canonical link tag to the Yii::$app->view:

namespace app\components;

use Yii;

class SEOUrlRule extends \yii\web\UrlRule
{
    public $permanents  = [];
    public $temporaries = [];
    public $secondaries = [];

    public $canonical = null;

    public function parseRequest($manager, $request)
    {
        $pathInfo = $request->getPathInfo();

        if(in_array($pathInfo, $this->permanents)) 
        {
            $request->setPathInfo($this->name);
            Yii::$app->response->redirect($this->name, 301);
        }

        else if(in_array($pathInfo, $this->temporaries)) 
        {
            $request->setPathInfo($this->name);
            Yii::$app->response->redirect($this->name, 302);
        }

        else if($this->canonical or in_array($pathInfo, $this->secondaries)) 
        {
            $route = $this->name;

            if ($this->canonical === null) $request->setPathInfo($route);
            else $route = $this->canonical;

            Yii::$app->view->registerLinkTag([
                'rel' => 'canonical', 
                'href' => Yii::$app->urlManager->createAbsoluteUrl($route)
            ]);
        }

        return parent::parseRequest($manager, $request);
    }
}

And that is all what it needs. Note that Yii::$app->controller or its related actions won't be yet available at this early stage of solving routes as it is shown in this lifecycle diagram but it seems that Yii::$app->view is already initialized and you can use its $params property to set custom parameters (as it is done in this example) which may be useful for more advenced cases where more data should be shared or populated to final output.

Bronder answered 17/3, 2016 at 11:33 Comment(3)
Thank you for the exhaustive answer, looks great to me. I wonder if the behaviours (still don't feel enough comfortable with them) could provide more concise way to handle it.Cusec
you're welcome. I think injecting canonical tag from SEOUrlRule class is not an option as it is in a very early stage that Yii::$app->controller should not exist yet so the check should be done later preferably inside controller. now the question is: How many actions do you have to render those pages previews ?Bronder
you are right. there was a simpler and cleaner way to handle it. no controller or action yet but the view instance was already there and I think it won't be a simpler way than directly registering the link to it.Bronder
E
4

I think you will have problems when creating the URL from the application to "article/a".

Why not use htaccess or the vhost file to do a 302 redirect to the proper URL?

If you want to handle it through the urlManager, I think you can just register the canonical link

$this->registerLinkTag(['rel' => 'canonical', 'href' => 'article/a']); 

in the view. Mode details here: http://www.yiiframework.com/doc-2.0/yii-helpers-baseurl.html#canonical()-detail

Eklund answered 11/3, 2016 at 4:53 Comment(3)
I'm using htaccess for now (it seemed ok for PHP4 apps), and currently it looks like a steaming pile of redirects. I don't really like the idea of writing 'article/a' explicitly in controller and coupling it with router, I'm quite sure that it is router's (UrlManager) job.Cusec
You are redirecting everything to 'article/a' so I assume you have an action a in your article controller. In the view of that action add $this->registerLinkTag(...... href=URL::to([put things here that will create the proper link for you])). Why don't you like this idea?Eklund
not to hard-code canonical URL to controller requirement is there for a reason. Canonical URL is formed by SEO guys who have access to routing configuration (urlManager array) but don't have to edit controllers (and I don't want them to). In bigger project I would store all slugs in DB and edit them from backend, but for this project it's like that. If there should be reasons to decouple router and controllers, that's the one.Cusec
O
2

Yii2 provides a tool to generate canonnical urls based on your rules.

\helpers\Url::canonical()

The idea is that it will provide you an url to 'article-a'.

Ochrea answered 15/3, 2016 at 6:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.