Элементы SEO для Yii Framework

SEO

При построении любого сайта в какой-то момент разработчик сталкивается с требованиями поисковой оптимизации. Традиционно она включает в себя построение правильной адресной структуры, исключение из индексации служебных страниц и неинтересных для поискового робота фрагментов, добавление метаинформации для записей. Более расширенный вариант подразумевает специфическое распределение ссылочного веса и борьбу с дубликатами адресов.

Заголовок окна и метатеги

По правилам хорошего тона каждая наша запись должна сопровождаться корректным заголовком окна, описанием и ключевыми словами. То есть приблизительно так:

<head>
    <title>Разведение кроликов | Блог о кроликах</title>
    <meta name="description" content="Статья о кроликах" />
    <meta name="keywords" content="кролики, разведение, питание" />
</head>

В представлениях мы можем добавлять метатеги как явно, так и как скрипты и стили с помощью компонента clientScript.

Достаточно вписать вручную только формирование заголовка. Для этого в классе CController уже имеется свойство pageTitle, так что мы без проблем можем использовать его в шаблоне:

<head>
    <title><?php echo CHtml::encode($this->pageTitle); ?> | <?php echo Yii::app()->name; ?></title>
</head>

А присваивать этот заголовок и добавлять новые теги можем уже в конкретных представлениях:

<?php
$this->pageTitle = $model->pagetitle;
Yii::app()->clientScript->registerMetaTag($model->description, 'description');
Yii::app()->clientScript->registerMetaTag($model->keywords, 'keywords');
?>
 
<h1><?php echo CHtml::encode($model->title); ?></h1>

Для хранения значений нужно не забыть добавить соответствующие поля в таблицу и модель наших статей. Если отдельные значения записывать не хочется, то можно генерировать их автоматически:

<?php
$this->pageTitle = $model->title;
Yii::app()->clientScript->registerMetaTag(strip_tags(mb_substr($model->text, 0, 200, 'utf-8')) . '...', 'description');
Yii::app()->clientScript->registerMetaTag(implode(', ', CHtml::listData($model->tags, 'id', 'name')), 'keywords');
?>

Хотя это не очень хорошо с точки зрения поисковой оптимизации.

Если конструкция с Yii::app()->clientScript кажется слишком громоздкой, то можно пойти по стопам свойства pageTitle и ввести новые поля description и keywords в базовый контроллер:

class Controller extends CController
{
    public $menu = array();
    public $breadcrumbs = array();
 
    public $description = '';
    public $keywords = '';
}

Теперь в представлениях достаточно присваивать значения этим полям:

<?php
$this->pageTitle = $model->pagetitle;
$this->description = $model->description;
$this->keywords = $model->keywords;
?>
 
<h1><?php echo CHtml::encode($model->title); ?></h1>

А присвоенные значения уже выводить в шаблоне через clientScript или вручную:

<head>
    <title><?php echo CHtml::encode($this->pageTitle); ?> | <?php echo Yii::app()->name; ?></title>
    <meta name="description" content="<?php echo CHtml::encode($this->description); ?>" />
    <meta name="keywords" content="<?php echo CHtml::encode($this->keywords); ?>" />
</head>

Теперь для всех страниц и новостей можно будет прописывать метатеги.

Дубли значений метатегов

Какие значения <title> будет у первых четырёх страниц при паджинации блога про кроликов? Приблизительно такие:

Разведение кроликов
Разведение кроликов
Разведение кроликов
Разведение кроликов

Все разделы с перелистыванием страниц будут нести одинаковые имена.

Если вы решили закрыть все кроме первой страницы директивой Disallow в файле robots.txt, то переживать не стоит.

Аналогично с описаниями description категории. В панели вебмастеров Google вы встретите негодование поисковой системы по этому поводу. Из-за повторов заголовка в индексе будет находиться всего одна страница.

Самое лучшее решение в случае необходимости индексации всех страниц заключается в написании уникального заголовка для каждой. Это нам врядли под силу, поэтому проще сделать автоматическое добавление номера страницы:

Разведение кроликов
Разведение кроликов - Страница 2
Разведение кроликов - Страница 3
Разведение кроликов - Страница 4

Для этого достаточно получить номер и добавить его к значениям соответствующих полей:

<?php
$page = (int)Yii::app()->request->getQuery('page', 1);
 
$this->pageTitle = $model->pagetitle . ($page > 1 ? ' - Страница ' . $page : '');
$this->description = $model->description . ($page > 1 ? ' - Страница ' . $page : '');
$this->keywords = $model->keywords;
?>

Удобнее переместить всю логику в помощник:

class PageHelper
{
    static public function pageString($param = 'page')
    {
        $page = (int)Yii::app()->request->getQuery($param, 1);
        return $page > 1 ? ' - Страница ' . $page : '';
    }
}

и использовать его:

<?php
$this->pageTitle = $model->pagetitle . PageHelper::pageString('page');
$this->description = $model->description . PageHelper::pageString('page');
$this->keywords = $model->keywords;
?>

Теперь на сайте не будет дублирующихся заголовков.

Это не так уж хорошо решает проблемы ранжирования, но немного помогает.

Установка rel=nofollow для ссылок из блоков кода

Представим, что у нас есть какой-либо блок текста или облако меток со ссылками. Порой для исключения их из индексации и из распределения ссылочного веса страниц этот блок нужно поместить в <noindex> и всем ссылкам добавить атрибут rel="nofollow".

Для выполнения этой работы в представлении лучше всего подходит виджет. Напишем его:

class DNofollowWidget extends СWidget
{
    public function init() {
        ob_start();
        ob_implicit_flush(false);
    }
 
    public function run() {
        $html = ob_get_clean();
        $html = preg_replace('#<a(\s([^>]+))?\srel="[^"]*"#is', '<a$1', $html);
        $html = str_replace('<a ', '<a rel="nofollow" ', $html);
        echo $html;
    }
}

Мы включаем буферизацию, получаем переданный HTML-код, удаляем атрибут rel у ссылок и подставляем rel="nofollow".

Теперь достаточно окружить любое меню данным виджетом:

<noindex>
<?php $this->beginWidget('NofollowWidget'); ?>
    <?php $this->beginWidget('zii.widgets.CMenu', array(
        'items'=>Tag::model()->getMenuList(),
    ); ?>
<?php $this->endWidget(); ?>
</noindex>

и облако меток или вспомогательное меню не будет больше распылять ссылочный вес.

Определение текущего маршрута

Порой в представлении или шаблоне бывает нужно узнать, из какого действия и какого контроллера это представление вызвали.

В WordPress для определения текущего положения имеются функции is_front_page(), is_category и некоторые другие. Это удобно использовать, например, чтобы не использовать ссылку с логотипа на главной странице.

Для этих целей в Yii удачно подходит параметр route контроллера. Он содержит текущий маршрут, состоящий из имени модуля, контроллера и действия. Если модулей в системе нет, то он содержит только контроллер и действие. Это свойство можно использовать для условных конструкций в представлениях.

Предположим, что у нас есть какой-либо текст в сайдбаре, и мы хотим закрыть его от индексирования для всех страниц, кроме главной.

Для этого в шаблоне мы можем обрамить данный блок конструкцией <noindex> (или её валидным вариантом <!--noindex-->) для всех маршрутов, кроме SiteController::actionIndex:

<?php if ($this->route != 'site/index'): ?><!--noindex--><? endif; ?>
<p>Приветствуем Вас!</p>
<?php if ($this->route != 'site/index'): ?><!--/noindex--><? endif; ?>

Аналогично с условным вызовом рассмотренного ранее виджета можно открыть ссылки во вспомогательном меню только на главной странице:

<?php if ($this->route != 'site/index') $this->beginWidget('DNofollowWidget'); ?>
 
    <?php $this->beginWidget('zii.widgets.CMenu', array(
        'items'=>Category::model()->getMenuList(),
    ); ?>
 
<?php if ($this->route != 'site/index') $this->endWidget(); ?>

Можно, например, вообще закрыть какое-либо меню на странице записи блога (то есть по маршруту blog/post/view) для избежания утечки ссылочного веса.

Аналогично можно использовать другие свойства контроллера:

$this->id;
$this->action->id;
$this->module->id;

Что равносильно прямому вызову соответствующих геттеров:

$this->getId();
$this->getAction()->getId();
$this->getModule()->getId();

С их помощью можно легко определить имя текущего контроллера, действия и модуля.

Находясь в виджете переменная $this ссылается на сам виджет, а не на контроллер. Нужно сначала получить контроллер, а уже потом обращаться к нему:

Yii::app()->controller->id;
Yii::app()->controller->action->id;
Yii::app()->controller->module->id;

Указание rel=nofollow для активного пункта меню

При построении любого меню с помощью виджета CMenu возникает ситуация, что активный элемент остаётся активной ссылкой и по-прежнему ссылается на текущую страницу. Получается бесконечная циклическая ссылка со страницы, ведущая на саму себя.

Переопределим и немного дополним метод CMenu::normalizeItems в нашем новом классе:

Yii::import('zii.widgets.CMenu');
 
class Menu extends CMenu
{
    protected function normalizeItems($items,$route,&$active)
    {
        foreach($items as $i=>$item)
        {
            if(isset($item['visible']) && !$item['visible'])
            {
                unset($items[$i]);
                continue;
            }
            if(!isset($item['label']))
                $item['label']='';
            if($this->encodeLabel)
                $items[$i]['label']=CHtml::encode($item['label']);
            $hasActiveChild=false;
            if(isset($item['items']))
            {
                $items[$i]['items']=$this->normalizeItems($item['items'],$route,$hasActiveChild);
                if(empty($items[$i]['items']) && $this->hideEmptyItems)
                {
                    unset($items[$i]['items']);
                    if(!isset($item['url']))
                    {
                        unset($items[$i]);
                        continue;
                    }
                }
            }
            if(!isset($item['active']))
            {
                if($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item,$route))
                    $active=$items[$i]['active']=true;
                else
                    $items[$i]['active']=false;
            }
            elseif($item['active'])
                $active=true;
            if(isset($items[$i]['active']) && $items[$i]['active'])
                $items[$i]['linkOptions']['rel']='nofollow';
 
        }
        return array_values($items);
    }
}

Мы взяли код из исходного метода и дописали пару строк:

if(!isset($items[$i]['active']) && $items[$i]['active'])
    $items[$i]['linkOptions']['rel']='nofollow';

в которых активным ссылкам добавляется соответствующий атрибут.

Теперь вместо zii.widgets.CMenu можно использовать свой класс Menu:

<?php $this->beginWidget('Menu', array(
    'items'=>Category::model()->getMenuList(),
); ?>

и активная ссылка будет автоматически снабжена атрибутом rel="nofollow".

Noindex для «хвоста» хлебных крошек

При использовании «хлебных крошек» с включённым выводом текущего заголовка у нас на странице будет дублирование текста. Например, так:

<div class="breadcrumbs">
    <a href="/">Главная</a> / <a href="/blog">Блог</a> / <span>Размножение кроликов в дикой природе</span>
</div>
<h1>Размножение кроликов в дикой природе</h1>

Одна и та же фраза (заголовок) написана два раза рядом, что не очень хорошо.

Для борьбы с таким контентным «спамом» можно заменить шаблон последнего элемента:

<?php $this->widget('zii.widgets.CBreadcrumbs', array(
    'links'=>$this->breadcrumbs,
    'inactiveLinkTemplate'=>'<!--noindex--><span>{label}</span><!--/noindex-->',
)); ?>

Если используются темы, то можно вернуть стандартное включение:

<?php $this->widget('zii.widgets.CBreadcrumbs', array('links'=>$this->breadcrumbs)); ?>

и использовать глобальную конфигурацию или скин views/skins/CBreadcrumbs.php:

<?php
return array(
    'default'=>array(
    'inactiveLinkTemplate'=>'<!--noindex--><span>{label}</span><!--/noindex-->',
),
);

Теперь активный пункт хлебных крошек будет неиндексируемым.

Борьба с дубликатами страниц

Как мы уже говорили в статье о маршрутизации, использование так называемых «правил по умолчанию» и достаточно гибкая система маршрутизации даёт нам весьма спорную возможность обращаться к одному и тому же действию по разным адресам:

/blog/post/1
/blog/post/view?id=1
/index.php/blog/post/1
/index.php?r=/blog/post/1
/index.php?r=/blog/post/view?id=1

Это относится и к главной странице сайта:

/
/site
/site/index
/index.php/site
/index.php/site/index
/index.php?r=/site
/index.php?r=/site/index

Если случайно или специально (из Твиттера вашего соседа, знающего Yii) эти адреса попадут в индекс поисковой системы, то поисковик разочаруется десятками одинаковых страниц по разным адресам и наложит санкции на ваш сайт за повторяющийся на разных страницах контент.

Как решить этот вопрос? Можно не решать и оставить как есть (ведь в ссылках на сайте все адреса будут правильными), если это соседям не понадобится. Но, согласно закону Мерфи, если есть вероятность чего-то особенно страшного, то это страшное наступит в самый неподходящий момент.

Рассмотрим несколько путей решения проблемы уникализации адресов.

Переадресация средствами сервера

Довольно банальный способ. Заключается в том, что нужно просто добавить редиректы в файл .htaccess. Сложно, муторно, но возможно.

Использование StrictParsing

Более эффективно использовать уже имеющиеся настройки компонентов. Кроме отключения showScriptName для скрытия index.php из адреса полезно включить строгую маршрутизацию useStrictParsing:

'urlManager'=>array(
    'urlFormat'=>'path',
    'showScriptName'=>false,
    'useStrictParsing'=>true,
    'urlSuffix'=>'',
    'rules'=>(
        ...
    ),
)

В этом режиме будут срабатывать только те правила, которые прописаны в rules, а адреса вроде /index.php/site больше не будут распознаваться как корректные. Это нам и нужно.

Но рассмотрим ещё раз правила по умолчанию (если вы их используете):

'' => 'site/index',
'<controller:\w+>' => '<controller>/index',
'<controller:\w+>/<id:\d+>' => '<controller>/view',
'<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>',
'<controller:\w+>/<action:\w+>' => '<controller>/<action>',

Первое, второе и последнее правила позволяют вполне «законно» использовать несколько вариантов адресов:

/
/site
/site/index
/blog
/blog/index

Поэтому нам необходимо отказаться от имеющихся правил по умолчанию и явно указать правила так, чтобы закрыть все возможные обходные пути, например:

'' => 'site/index',
'<action:login|logout|register>' => 'site/<action>',
 
'blog' => 'blog/index',
'blog/page/<page:\d+>' => 'blog/index',
'blog/post/<id:\d+>' => 'blog/view',
'blog/<action:category|tag>/page/<page:\d+>' => 'blog/<action>',
'blog/<action:category|tag>' => 'blog/<action>',

Теперь имеются только строго заданные маршруты и одно и то же действие нельзя открыть по двум разным адресам.

Здесь мы также включили в адрес и параметр page, так как хотим, чтобы он входил в адрес, а не подписывался как отдельный GET-параметр через вопросительный знак.

Универсальные правила можно оставить для контроллеров или модулей панели управления. Но правила по умолчанию можно и оставить, если использовать канонические адреса, которые мы рассмотрим ниже.

Другой пример с модулями мы рассмотрели в статье о маршрутизации, на которую ссылались выше.

Псевдонимы записей

Предположим, что в своём блоге к ID записи нам нужно добавить человекопонятный «хвост», то есть вместо банального:

/blog/24

Выводить адрес с псевдонимом:

/blog/24/monkeys-in-wild-nature

Для этого достаточно добавить правило:

'blog/<id:\d+>/<alias:[\w_-]+>' => 'blog/view',

И использовать псевдоним при создании адреса:

class Post
{
    public function getUrl() {
        return Yii::app()->createUrl('blog/view', array(
            'id'=>$this->id,
            'alias'=>$this->alias,
        ));
    }
}

Теперь в действии view как и раньше получаем нужную запись по ID и выводим её на экран:

class BlogController extends Controller
{
    public function actionView($id) {
        $model = Post::model()->findByPk($id);
        if (!$model)
            throw new CHttpException(404, 'Not found');
        $this->render('view', array(
            'model'=>$model,
        ));
    }
}

Всё бы хорошо, но так как здесь не учитывается переданный псевдоним, то его можно указать любым:

/blog/24/monkeys-in-wild-nature
/blog/24/who-killed-kennedy

По всем возможным адресам будет открываться одна и та же запись.

Поэтому нужно либо добавить учёт псевдонима alias:

class BlogController extends Controller
{
    public function actionView($id, $alias)
    {
        $model = Post::model()->findByAttributes(array(
            'id'=>$id,
            'alias'=>$alias,
        ));
 
        if ($model === null)
            throw new CHttpException(404, 'Not found');
 
        $this->render('view', array(
            'model'=>$model,
        ));
    }
}

При этом при любой опечатке в псевдониме мы получим ошибку.

Либо, что интереснее, можно добавить ещё одно правило:

'blog/<id:\d+>/<alias:[\w_-]+>' => 'blog/view',
'blog/<id:\d+>' => 'blog/view',

и ввести проверку адреса с принудительной переадресацией на правильный адрес

class BlogController extends Controller
{
    public function actionView($id, $alias='')
    {
        $model = Post::model()->findByPk($id);
 
        if ($model === null)
            throw new CHttpException(404, 'Not found');
 
        if (Yii::app()->request->url != $model->url)
            $this->redirect($model->url, true, 301);
 
        $this->render('view', array(
            'model'=>$model,
        ));
    }
}

Тогда какой бы адрес мы ни ввели в адресную строку, с псевдонимом или без:

/blog/24
/blog/24/who-killed-kennedy

нас всегда будет перекидывать с неправильных адресов на единственно привильный:

/blog/24/monkeys-in-wild-nature

Канонические ссылки

Успешно зарекомендовавший себя в SEO инструмен для внутренней оптимизации – это канонические ссылки. Например, мы можем зайти на сайт по ссылке в твиттере, рассылке или в рекламном блоке. Если эта система так работает или если вебмастеру хочется отслеживать источник прихода посетителя, то могут быть использованы дополнительные GET-параметры, например:

http://rmcreative.ru/blog/tag/Yii?from=twitter
http://rmcreative.ru/blog/tag/Yii?utm_source=subscribe&utm_campaign=mybirthday

Чтобы поисковые системы вместо них засчитывали это как ссылку на «чистый» адрес, нужно добавить в HTML-код страницы каноническую ссылку на первоисточник:

<head>
    ...
    <link rel="canonical" href="http://rmcreative.ru/blog/tag/Yii" />
</head>

Прстейший способ это сделать – это отбросить все GET-параметры, идущие после вопросительного знака (за это отвечает метод getPathInfo) и вставить исходный адрес прямо в код шаблона:

<link rel="canonical" href="<?php echo Yii::app()->request->getHostInfo() . '/' . Yii::app()->request->getPathInfo(); ?>" />

Либо разместить динамическое добавление поля в любом месте кода:

Yii::app()->clientScript->registerLinkTag('canonical', null, Yii::app()->request->getHostInfo() . '/' . Yii::app()->request->getPathInfo());

Например в методе beforeAction базового контроллера.

Теперь с какими бы GET-параметрами к нам не зашли, в индексе будет фигурировать только страница с «чистым» адресом.

Упорядочивание GET параметров

Если сложно прописать в правилах маршрутизации для нескольких параметров все возможные варианты, то можно добавить звёздочку в конце:

'shop/catalog/*' => 'shop/index',

и Yii воспримет это так, что все параметры станет достраивать в путь в виде параметр/значение/параметр/значение, то есть будут доступны следующие адреса:

/shop/catalog
/shop/catalog/type/5
/shop/catalog/type/5/page/2
/shop/catalog/type/5/size/16
/shop/catalog/type/5/category/15
/shop/catalog/type/5/category/15/color/6
/shop/catalog/type/5/category/15/size/16
/shop/catalog/type/5/category/15/color/red/size/16

Теперь из метода getUrl типа товара мы можем вернуть адрес с параметром type:

public function getUrl() {
    return Yii::app()->createUrl('shop/index', array(
        'type'=>$this->id,
    ));
}

что превратится в ссылку /shop/catalog/type/5.

А из аналогичного метода категории (которая, например, должна обязательно содержать тип) можно вернуть вторую ступень:

public function getUrl() {
    return Yii::app()->createUrl('shop/index', array(
        'type'=>$this->type_id,
        'category'=>$this->id,
    ));
}

А ссылка выбора размера и цвета должна работать в любом месте каталога. Так как мы не можем указать, какие параметры из имеющихся нам нужны, то возьмём все, пришедшие в массиве $_GET, и добавим к ним параметр размера size:

public function getUrl() {
    $params = array_replace($_GET, array('size'=>$this->id));
    return Yii::app()->createUrl('shop/index', $params;
}

Теперь вывести ссылки в подменю для любой сущности не составит труда:

<ul>
    <?php foreach ($categories as $category): ?>
        <li><?php echo CHtml::link(CHtml::encode($category->name), $category->url); ?></li>
    <?php endforeach; ?>
</ul>

В контроллере теперь можно принимать значения и добавлять их в условия:

class ShopController extends CController
{
    public function actionIndex()
    {
        $category = Yii::app()->request->getQuery('category');
        $type = Yii::app()->request->getQuery('type');
        $size = Yii::app()->request->getQuery('size');
        $color = Yii::app()->request->getQuery('color');
 
        $criteria = new CDbCriteria();
 
        $criteria->compare('t.category_id', $category);
        $criteria->compare('t.type_id', $type);
        $criteria->compare('t.size', $size);
        $criteria->compare('t.color', $color);
 
        $criteria->order = 't.id DESC';
 
        $dataProvider = CActiveDataProvider('Product', array(
            'criteria'=>$criteria,
            'pagination'=>array(
                'pageSize'=>Yii::app()->params['products_per_page'],
                'pageVar'=>'page',
            ),
        )
 
        $this->render('index', array(
            'dataProvider'=>$dataProvider,
        ));
    }
}

Теперь всё стало просто, но появились две проблемы.

Во-первых, параметры можно спокойно менять местами:

/shop/catalog/type/5/category/15/color/6
/shop/catalog/category/15/color/6/type/5

и они будут продолжать работать.

Во-вторых, можно дописывать любое число несуществующих параметров

/shop/catalog/type/5/category/15/color/6
/shop/catalog/type/5/category/15/color/6/vasya/fool

В совокупности эти проблемы позволяют довести число работающих вариантов адресов до нескольких сотен или тысяч.

Чаще всего нужно индексировать всего тип и категорию, так как только для них пишут SEO-тексты. Остальные параметры можно и не учитывать.

Итак, сейчас можно в качестве канонического адреса указывать адрес типа или категории в контроллере или представлении:

if ($category)
    $url = Yii::app()->request->getHostInfo() . '/' . $category->url);
elseif ($type)
    $url = Yii::app()->request->getHostInfo() . '/' . $type->url);
else
    $url = $this->createAbsoluteUrl('index');
 
Yii::app()->clientScript->registerLinkTag('canonical', null, $url);

Либо ввести белый список параметров и собирать текущий канонический адрес только для них, исключая все лишние параметры.

Чтобы это было проще и работало со всеми контроллерами, можно просто указывать имеющие значение параметры прямо в аргументах действий. Перепишем немного определение метода actionIndex:

class ShopController extends CController
{
    public function actionIndex($type=null, $category=null)
    {
        $size = Yii::app()->request->getQuery('size');
        $color = Yii::app()->request->getQuery('color');
 
        $criteria = new CDbCriteria();
        ...
    }
}

Здесь параметры type и category мы внесли в сигнатуру метода и объявили их необязательными.

Теперь в методе beforeAction родительского (или этого же) контроллера можно реализовать анализ аргументов текущего действия и генерировать канонический адрес:

class Controller extends CController
{
    protected function beforeAction($action) {
        // получаем класс контроллера
        $controllerRef = new ReflectionClass(get_class($this));
        // получаем метод-действие action*
        $actionRef = $controllerRef->getMethod('action' . ucfirst($action->getId()));
        // считываем список параметров действия
        // и берём из $_GET только эти параметры
        $params = array('page');
        foreach ($actionRef->getParameters() as $parameterRef) {
            $key = $parameterRef->name;
            if (isset($_GET[$key])) {
                $params[$key] = $_GET[$key];
            }
        }
        // строим адрес с выбранными параметрами с этим же маршрутом
        $url = $this->createAbsoluteUrl($this->route, $params);
        // регистрируем тег
        Yii::app()->clientScript->registerLinkTag('canonical', null, $url);
        return parent::beforeAction($action);
    }
}

Теперь на какой бы адрес нашего каталога посетители ни зашли, всегда в качестве канонического адреса будет указываться единственно верный вариант: только с разрешёнными (указанными в качестве аргументов метода) параметрами и только в том порядке, в котором они перечислены в сигнатуре метода.

Эта конструкция пока не работает с действия, вынесенные в отдельные классы, но используя полученный $action при желании это можно исправить.

С этим способом можно оставить правила маршрутизации по умолчанию. Действия тогда могут открываться по нескольким адресам, но каноническая ссылка на всех вариантах будет всегда выводиться единственно правильная.

Добавим теперь на сайт ещё одну полезную вещь.

Пинг поисковых систем

В системе управления сайтом WordPress, на радость блогерам, имеется встроенная система оповещения поисковых систем о добавлении новых статей. Она помогает быстрой индексации. Как она работает?

Ничего особо сложного. У многих поисковых систем есть автоматизированная система API для приёма обращений. С его использованием можно ознакомиться, например, у Яндекса. Аналогичную страницу можно поискать и для Google. Отличий там, в принципе, нет, так как это стандартизированный протокол Weblogs.Ping.

Для нашего проекта необходимо сделать компонент для работы с протоколом XMLRPC (XML Remote Procedure Call – буквально звучит как «Удалённый вызов процедур» с передачей запросов и получением ответов в формате XML). Настроек нашему компоненту слишком много не нужно. Достаточнно существования флага вкючения и указания списка серверов:

return array(
    'components'=>array(
        'rpcManager'=>array(
            'class'=>'DRPCManager',
            'pingEnable'=>true,
            'pingServers'=>array(
                'http://ping.blogs.yandex.ru/RPC2',
                'http://blogsearch.google.com/ping/RPC2'
            )
        ),
    ),
);

Для быстрой реализации работы с XMLRPC на низком уровне лучше всего взять готовую библиотеку IXR_Library. Теперь нашему компоненту ничего не остаётся, как просто создавать экземпляр клиента и вызывать через него метод weblogUpdates.ping удалённого сервера, передавая ему в качестве аргументов адрес страницы, имя и доменное имя сайта:

Yii::import('application.vendors.IXR_Library', true);
 
class DRPCManager extends CApplicationComponent
{
    public $pingEnable = true;
    public $pingServers = array();
 
    public function pingPage($pageURL)
    {
 
        $siteName = Yii::app()->name;
        $siteHost = Yii::app()->request->getHostInfo();
        $fullPageUrl = $siteHost . $pageURL;
 
        if ($this->pingEnable)
        {
            if (!$pageURL)
                return;
 
            foreach ($this->pingServers as $serverUrl)
            {
                if (preg_match('|(?P<host>\w+://[\w\.-]+)/?(?P<uri>.*)|i', $serverUrl, $matches))
                {
                    $client = new IXR_Client($matches['host'], $matches['uri']);
                    if (!$client->query('weblogUpdates.ping', array($siteName, $siteHost, $fullPageUrl)))
                        Yii::log('Ping error for ' . $serverUrl, CLogger::LEVEL_WARNING);
                }
            }
        } else
            Yii::log('Emulation of ping for ' . $fullPageUrl);
    }
}

Нам пришлось немного «попотеть» с разделением адресов на доменное имя и строку запроса, но это специфика библиотеки.

Обратите внимание на импорт файла:

Yii::import('application.vendors.IXR_Library', true);

Поместить его вы можете куда угодно, не только в protected/vendors. Главное импортировать этот файл с параметром true, чтобы Yii сделал вызов include сразу же. Без этого он не найдёт класс IRX_Client, так как имя данного класса не совпадает с именем файла.

Что теперь необходимо для включения этого компонента в работу? Если у нашего сайта есть новости и блог, то достаточно просто произвести вызов в момент сохранения наших моделей:

class Post extends CActiveRecord
{
    protected function afterSave()  {
        if ($this->isNewRecord)
            Yii::app()->rpcManager->pingPage($this->getUrl());
        parent::afterSave();
    }
 
    private $_url;
 
    public function getUrl(){
        if ($this->_url === null)
            $this->_url = Yii::app()->createUrl('post/view', array('id'=>$this->id));
        return $this->_url;
    }
}

Так как для генерации адреса нам нужно значение $model->id (которое появляется только после записи в базу данных) мы вынуждены делать все в afterSave.

Но чтобы не «загрязнять» метод afterSave всякими низкоуровневыми «штучками» можно пойти дргим путём. Мы ведь уже изучили поведения в Yii, чтобы делать наши приложения разборными как конструкторы. Просто напишем поведение:

class DPingBehavior extends CActiveRecordBehavior
{
    public $urlAttribute = 'url';
    public $pingByCreate = true;
    public $pingByUpdate = false;
 
    public function afterSave($event)
    {
        $model = $this->getOwner();
        if ($this->pingByCreate && $model->isNewRecord || $this->pingByUpdate) {
            Yii::app()->rpcManager->pingPage($model->{$this->urlAttribute});
        }
    }
}

Мы сделали его достаточно настраиваемым. Подключим теперь это к модели:

class Post extends CActiveRecord
{
    public function behaviors() {
        return array(
            'PingBehavior'=>array(
                'class'=>'DPingBehavior',
                'urlAttribute'=>'url',
            ),
        );
    }
 
    private $_url;
 
    public function getUrl(){
        if ($this->_url === null)
            $this->_url = Yii::app()->createUrl('/post/view', array('id'=>$this->id));
        return $this->_url;
    }
}

Здесь мы указали ему брать адрес из свойства $model->url. Этот вызов обратится к геттеру $model->getUrl() (так уж заведено в Yii) и все сработает корректно.

По умолчанию поведение будет производить обращение к поисковой системе только при создании модели. Если же нужно это делать при каждом обновлении статьи, то добавьте поле pingByUpdate со значенем true.

Вот так быстро мы сделали возможность оповещения поисковых систем о новых статьях на сайте.

Заключение

Здесь мы рассмотрели только некоторые ключевые моменты, которые помогут сделать качественно оптимизированный для поисковых систем проект. Если не знаете, как можно это применить, то можно ознакомиться со пользой специфического распределения веса на сайте:

Другие статьи

Недавно в обратную связь поступил вопрос. Один из читателей поинтересовался, как можно сделать удобный вывод иерархических данных, построенных по принципу Adjacency List, в виджете CGridView вместо CTreeView. Это, например, могут быть вложенные статические страницы, категории или пункты меню, хранимые в базе данных. Попробуем решить этот вопрос.

Мир PHP-разработки с давних времён славится хаотической практикой создания зависимых самописных неуниверсальных систем. И приверженцы остальных языков над ним смеялись. В последние годы, что действительно хорошо, он всё более масштабно переходит к другому, более осознанному будущему. Интересная для многих практика социального программирования с использованием публичных репозиториев привнесла идею обобщении полученных знаний.

Вот и прошёл недавно первый год второй жизни блога. В то время, пока вы делитесь эмоциями в комментариях ко вчерашней статье о миграциях, пора мне выйти из тени и поделиться парой новостей. Итак, если у вас есть какие-нибудь предложения к развитию блога в будущем году, если у вас уже праздничное настроение, то прошу пожаловать под кат.

Три программиста пишут код проекта. И с кодом у них нет проблем. Есть три рабочих машины, центральный репозиторий и главный рабочий сервер, берущий файлы из этого же репозитория. Работа кипит. у каждого на своём компьютере установлен PhpMyAdmin, что им позволяет время от времени вносить изменения в свою базу данных.

Комментарии

 

Иван

Спасибо, хорошая статья. Подчеркнул кое-что интересное для себя.

Ответить

 

Илья

Спасибо, Дмитрий. Второй вариант вывод мета тегов (не громозкий) Супер. Люблю красивый код.

Ответить

 

script

Спасибо. Очень детально.
Дмитрий, а будет что-то по Yii2?

Ответить

 

Дмитрий Елисеев

Будет. Но уже после праздников, как я и говорил в предпраздничных новостях. Надо ещё на официальные релизы второй версии ориентироваться.

Ответить

 

Redee
<a rel="nofollow"
<!--noindex-->

Будем знать, конечно пока что для меня лично не актуально так как нет особо больших проектов, но на будущее весьма полезно.

з.ы. Прогу как то поумнее продавал бы создатель )).

Ответить

 

Дмитрий Елисеев

Ну не знаю, как её именно создатель продаёт.

Ответить

 

foji

Спасибо!) Ждал именно этого!

Ответить

 

Алексей

Дмитрий, спасибо за очередной труд!

Ответить

 

Стас

С наступающим! Отличная подача, впрочем как всегда!

Ответить

 

BS

К выше перечисленному можно было добавить пагинацию с rel="prev" и rel="next"

Ответить

 

BS

pastebin.com/8uGYAmCa - вот такое, не помню чьё, взял в оф. расширения и немного подправил.

Ответить

 

BS

табов чересчур наделал автозаменой :(

Ответить

 

Илья

Привет, отличный материал. Нашел опечатку:

if(!isset($items[$i]['active']) && $items[$i]['active'])
$items[$i]['linkOptions']['rel']='nofollow';

Должно быть: isset($items[$i]['active']), лишнее отрицание

Ответить

 

Дмитрий Елисеев

Спасибо! Исправил.

Ответить

 

Илья

В моем случае меню многоуровневое и все родительские элементы помечаются, как активные, соответственно у них так же появляется атрибут rel=nofollow. Чтобы этого избежать изменил ваш пример с меню: http://pastebin.com/3rYLPNNg

Ответить

 

kenny

А еще чпу можно реализовать по подобию Drupal'овского модуля Path. В одном проекте делали наследника CBaseUrlRule, и дергали алиасы из бд.

Конечно запросов к бд стало на порядок больше, но клиенту хотелось чтобы в адресе не было цифр.

Ответить

 

vovik

Спасибо за статью, как же все сложно в этим yii

Ответить

 

evgenij
if (preg_match('|(?P<host>\w+://[\w\d\._-]+)(/?P<uri>.*)|i', $serverUrl, $matches))
                {


У меня одного через регулярные не проходит выражение ?
все работает окно это выражение всегда с пустыми массивами получается ... :( адреса которые проверяются не менял

Ответить

 

Дмитрий Елисеев
preg_match('|(?P<host>\w+://[\w\.-]+)/?(?P<uri>.*)|i'
Ответить

 

evgenij

Спасибо. Сейчас проходит :) Но теперь другая ошибка.
Реализация IXR_Client требует что бы подавался host без http://

Когда сделал так что все проходит..:

$client = new IXR_Client('ping.blogs.yandex.ru', '/RPC2');
Ответить

 

Дмитрий Елисеев

Понятно. Тогда так:

preg_match('|^\w+://(?P<host>[\w\.-]+)(?P<uri>/.*)$|i'
Ответить

 

Сергей

Хорошая статья, много полезных решений. Основная SEO-проблема в Yii, на мой взгляд, это формирование адреса через имя контроллера. Для каждого контроллера приходится городить костыли в urlManager, иначе второй уровень вложенности крайне негативно сказывается на результатах продвижения. Нет ли универсального способа решения данной проблемы?

Ответить

 

Дмитрий Елисеев

Вместо:

'<controller:\w+>/<action:\w+>' => '<controller>/<action>',

и подобных правил можно сделать:

'<controller:\w+>-<action:\w+>' => '<controller>/<action>',

то есть через дефис.

Ответить

 

Максим

По поводу пинга поисковых систем.

Эта штука работает только для блогов? Или можно применить для другого сорта контента.. типа библиотеки, новостей и подобное

Ответить

 

Дмитрий Елисеев

Это просто оповещение поисковой системы о том, что у вас появилась новая страница. Чтобы он знал, что надо проиндексировать. Так что для всех сайтов подойдёт.

Ответить

 

Максим

Кстати, у вас там ошибочка в коде компонента

           class DRPCManager extends CApplicationComponent

надо вынести за if код

            $siteName = Yii::app()->name;
            $siteHost = Yii::app()->request->getHostInfo();
            $fullPageUrl = $siteHost . $pageURL; 

иначе $fullPageUrl в лог не попадет.

Ответить

 

Максим

в тестовом режиме, когда public $pingEnable = false;

Ответить

 

Дмитрий Елисеев

Спасибо! Исправил.

Ответить

 

Максим

А что значит сообщение Ping error for ...

Ответить

 

Максим

ошибка: Error message: transport error - could not open socket

Ответить

 

Дмитрий Елисеев

Может быть отключено расширение php_socket в php.ini. Или если пингуете по https, то нужно использовать класс IXR_ClientSSL.

Ответить

 

Максим

Гугл использую

http://blogsearch.google.com/ping/RPC2
Ответить

 

Евгений

Спасибо, хорошая статья.
Вопрос:
нас всегда будет перекидывать с неправильных адресов на единственно привильный:

А если дописать слеш, будет зацикливание. Может лучше не редирект, а throw new CHttpException?

Ответить

 

Дмитрий Елисеев

Ну это на любителя. Мне больше нравится редирект.

Ответить

 

Андрей

Какой му....к придумал новый язык программирования внутри самого языка программирования?

Ответить

 

Дмитрий Елисеев

Это Вы про что?

Ответить

 

Андрей

про это юии

Ответить

 

Дмитрий Елисеев

Ну это как сказать. Всякие DSL, шаблонизаторы и прочее являются такими языками, а в Yii их почти нет. А считать ли специфичную архитектуру и наборы готовых компонентов новым языком – это уже вопрос риторический.

Ответить

 

Иван

А еще дубли появляются для списка страниц, если подставлять число страниц больше, чем есть на самом деле, то все равно открывается последняя страница. Вот например /blog/page-388888888888

Ответить

 

Дмитрий Елисеев

Да, проблема. Но у меня просто page-* закрыто в robots.txt.

Ответить

 

Илья

Привет.
В IXR_Library что-то видимо поменялось, ставил через composer require lsmonki/php-ixr:1.7

Там сейчас не обязятельно самому парсить url, но с другой стороны параметры передаются не как один массив, а метод с переменным числом параметров, итого сейчас правильно так:

    $client = new IXR_Client($serverUrl);
    if (!$client->query('weblogUpdates.ping', $siteName, $siteHost.$listUrl, $siteHost.$itemUrl, $siteHost.$atomUrl))
        Yii::log('Ping error for ' . $serverUrl, CLogger::LEVEL_WARNING);

тут мои добавления: listUrl -- веб-страница со списком элементов (например, новости), itemUrl -- урл самого добавляемого элемента, atomUrl -- url atom-feed. Вроде это все соответствует документации этого weblog.

Ответить

 

Григорий

Я конечно извиняюсь, а что, если сделать вот так, поисковики разве будут индексировать хвост крошек:

<div class="breadcrumbs">
<a href="/">Главная</a> &raquo; <a href="/articles/">Статьи</a> &raquo; <!--noindex--><span>Статья 1 общая</span><!--/noindex-->
</div>				
Ответить

 

Дмитрий Елисеев

Я так и делаю:

'inactiveLinkTemplate'=>'<!--noindex--><span>{label}</span><!--/noindex-->',
Ответить

 

manas_anarov

Спасибо, хорошая статья научился добавлять мета теги key и desc к своим постам.

Ответить

 

Richi

Можете еще добавить пагинацию с rel="prev" и rel="next":

if ($currentPage > 1) {
    Yii::app()->clientScript->registerLinkTag('prev', NULL,$this->createAbsoluteUrl(NULL, array_replace($_GET, array('page' => $currentPage - 1))));
}
if ($currentPage < $dataProvider->getPagination()->getPageCount()) {
    Yii::app()->clientScript->registerLinkTag('next', NULL,$this->createAbsoluteUrl(NULL, array_replace($_GET, array('page' => $currentPage + 1))));
}
Ответить

 

Web design Dubai

Хотел спросить по поводу Ping Behavior. В геттере getUrl вызывается createUrl без второго параметра, указывающего на полный путь, т.е. создается относительный путь. RPC примет разве такой урл?)

Ответить

 

Web design Dubai

Все, извиняюсь, вижу что в компоненте добавляется full url. Не думаю что это правильно, но да ладно.

Ответить

 

Web design Dubai

Мне кажется что пинговать надо в отдельном потоке, например в Gearman.

Ответить

 

Костя Никитин

А как что-то подобное реализjвать на YII2?

Ответить

 

ilma55

Добрый день.
подскажите пожалуйста как подключить IXR_Library в YII2 !!!

вроде подключаю так
use frontend\helpers\IXR_Library\IXR_Client;

но результат нолевой.

Ответить

 

Дмитрий Елисеев

Что значит "нулевой"?

Ответить

 

ilma55

делаю так

$pingClient = new IXR_Client('ping.blogs.yandex.ru', '/RPC2');
var_dump($pingClient);die();

результат

object(frontend\helpers\IXR_Library\IXR_Client)#86 (9) { ["server"]=> NULL ["port"]=> NULL ["path"]=> NULL ["useragent"]=> NULL ["response"]=> NULL ["message"]=> bool(false) ["debug"]=> bool(false) ["timeout"]=> NULL ["error"]=> bool(false) } 

возможно я не рпавильно подключаю библиотеку??
как я сделал: из фала IXR_Library.php я вынес все классы в разные файлы.

просто если делать не на фреимворке , то результат есть.

Ответить

 

Дмитрий Елисеев

А конструктор в классе не переопределяли?

Ответить

 

ilma55

нет. я только конструкторы привл к виду php 5.3
заменил название класса на __constructor

а не подскажите по какому принципу переопределить?

Ответить

 

ilma55

все. вроде сделал. ошибок нет. спасибо.

Ответить

 

Sergey Aver

А на вторую версию не будет похожей статьи? Меня вот интересует как сделать так чтобы meta-данные автоматом заполнялись из таблицы pages, а если есть контроллер, то данные брались из контроллера.

Ответить

 

Дмитрий Елисеев

Для второго регистрацию метатегов можно и так сделать в контроллере страницы:

$this->title = $model->meta_title;
$this->registerMetaTag(['name' => 'description', 'content' => $model->meta_description]);
$this->registerMetaTag(['name' => 'keywords', 'content' => $model->meta_keywords]);
$this->params['breadcrumbs'][] = $model->title;
Ответить

 

Анатолий

Здравствуйте!

При использовании первого рецепта вместо сайта отображается содержимое PageHelper.
Догадываюсь, что примитивный вопрос, но как это поправить?

Спасибо!

Ответить

 

Дмитрий Елисеев

Добавить

<?php

в начало файла.

Ответить

 

Анатолий

Переделал, теперь устойчиво:

Error 500
Undefined variable: model

Ответить

 

Дмитрий Елисеев

Найти представление view.php и найти там $model.

Ответить

 

Анатолий

Спасибо, это я поправил.

Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>