Элементы SEO для Yii Framework
При построении любого сайта в какой-то момент разработчик сталкивается с требованиями поисковой оптимизации. Традиционно она включает в себя построение правильной адресной структуры, исключение из индексации служебных страниц и неинтересных для поискового робота фрагментов, добавление метаинформации для записей. Более расширенный вариант подразумевает специфическое распределение ссылочного веса и борьбу с дубликатами адресов.
Заголовок окна и метатеги
По правилам хорошего тона каждая наша запись должна сопровождаться корректным заголовком окна, описанием и ключевыми словами. То есть приблизительно так:
<head> <title>Разведение кроликов | Блог о кроликах</title> <meta name="description" content="Статья о кроликах" /> <meta name="keywords" content="кролики, разведение, питание" /> </head>
В представлениях мы можем добавлять метатеги как явно, так и как скрипты и стили с помощью компонента clientScript
.
Достаточно вписать вручную только формирование заголовка. Для этого в классе CController
уже имеется свойство pageTitle
, так что мы без проблем можем использовать его в шаблоне:
<head> <title><?php echo CHtml::encode($this->pageTitle); | 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> 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> echo CHtml::encode($model->title); </h1>
А присвоенные значения уже выводить в шаблоне через clientScript
или вручную:
<head> <title><?php echo CHtml::encode($this->pageTitle); | echo Yii::app()->name; </title> <meta name="description" content=" echo CHtml::encode($this->description); " /> <meta name="keywords" content=" 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'); $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> if ($this->route != 'site/index'): <!--/noindex--> endif;
Аналогично с условным вызовом рассмотренного ранее виджета можно открыть ссылки во вспомогательном меню только на главной странице:
<?php if ($this->route != 'site/index') $this->beginWidget('DNofollowWidget'); $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> echo CHtml::link(CHtml::encode($category->name), $category->url); </li> 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
.
Вот так быстро мы сделали возможность оповещения поисковых систем о новых статьях на сайте.
Заключение
Здесь мы рассмотрели только некоторые ключевые моменты, которые помогут сделать качественно оптимизированный для поисковых систем проект. Если не знаете, как можно это применить, то можно ознакомиться со пользой специфического распределения веса на сайте:
Спасибо, хорошая статья. Подчеркнул кое-что интересное для себя.
Спасибо, Дмитрий. Второй вариант вывод мета тегов (не громозкий) Супер. Люблю красивый код.
Спасибо. Очень детально.
Дмитрий, а будет что-то по Yii2?
Будет. Но уже после праздников, как я и говорил в предпраздничных новостях. Надо ещё на официальные релизы второй версии ориентироваться.
Будем знать, конечно пока что для меня лично не актуально так как нет особо больших проектов, но на будущее весьма полезно.
з.ы. Прогу как то поумнее продавал бы создатель )).
Ну не знаю, как её именно создатель продаёт.
Спасибо!) Ждал именно этого!
Дмитрий, спасибо за очередной труд!
С наступающим! Отличная подача, впрочем как всегда!
К выше перечисленному можно было добавить пагинацию с rel="prev" и rel="next"
pastebin.com/8uGYAmCa - вот такое, не помню чьё, взял в оф. расширения и немного подправил.
табов чересчур наделал автозаменой :(
Привет, отличный материал. Нашел опечатку:
if(!isset($items[$i]['active']) && $items[$i]['active'])
$items[$i]['linkOptions']['rel']='nofollow';
Должно быть: isset($items[$i]['active']), лишнее отрицание
Спасибо! Исправил.
В моем случае меню многоуровневое и все родительские элементы помечаются, как активные, соответственно у них так же появляется атрибут rel=nofollow. Чтобы этого избежать изменил ваш пример с меню: http://pastebin.com/3rYLPNNg
А еще чпу можно реализовать по подобию Drupal'овского модуля Path. В одном проекте делали наследника CBaseUrlRule, и дергали алиасы из бд.
Конечно запросов к бд стало на порядок больше, но клиенту хотелось чтобы в адресе не было цифр.
Спасибо за статью, как же все сложно в этим yii
У меня одного через регулярные не проходит выражение ?
все работает окно это выражение всегда с пустыми массивами получается ... :( адреса которые проверяются не менял
Спасибо. Сейчас проходит :) Но теперь другая ошибка.
Реализация IXR_Client требует что бы подавался host без http://
Когда сделал так что все проходит..:
Понятно. Тогда так:
Хорошая статья, много полезных решений. Основная SEO-проблема в Yii, на мой взгляд, это формирование адреса через имя контроллера. Для каждого контроллера приходится городить костыли в urlManager, иначе второй уровень вложенности крайне негативно сказывается на результатах продвижения. Нет ли универсального способа решения данной проблемы?
Вместо:
и подобных правил можно сделать:
то есть через дефис.
По поводу пинга поисковых систем.
Эта штука работает только для блогов? Или можно применить для другого сорта контента.. типа библиотеки, новостей и подобное
Это просто оповещение поисковой системы о том, что у вас появилась новая страница. Чтобы он знал, что надо проиндексировать. Так что для всех сайтов подойдёт.
Кстати, у вас там ошибочка в коде компонента
надо вынести за if код
иначе $fullPageUrl в лог не попадет.
в тестовом режиме, когда public $pingEnable = false;
Спасибо! Исправил.
А что значит сообщение Ping error for ...
ошибка: Error message: transport error - could not open socket
Может быть отключено расширение php_socket в php.ini. Или если пингуете по https, то нужно использовать класс IXR_ClientSSL.
Гугл использую
Похоже ,что нашел ответ. Гугл закрыл Пинг АПИ
https://productforums.google.com/forum/#!msg/webmasters/4ekRAvcL1bA/P1Tk2rkVzjsJ
Спасибо, хорошая статья.
Вопрос:
нас всегда будет перекидывать с неправильных адресов на единственно привильный:
А если дописать слеш, будет зацикливание. Может лучше не редирект, а throw new CHttpException?
Ну это на любителя. Мне больше нравится редирект.
Какой му....к придумал новый язык программирования внутри самого языка программирования?
Это Вы про что?
про это юии
Ну это как сказать. Всякие DSL, шаблонизаторы и прочее являются такими языками, а в Yii их почти нет. А считать ли специфичную архитектуру и наборы готовых компонентов новым языком – это уже вопрос риторический.
А еще дубли появляются для списка страниц, если подставлять число страниц больше, чем есть на самом деле, то все равно открывается последняя страница. Вот например /blog/page-388888888888
Да, проблема. Но у меня просто page-* закрыто в robots.txt.
Привет.
В IXR_Library что-то видимо поменялось, ставил через composer require lsmonki/php-ixr:1.7
Там сейчас не обязятельно самому парсить url, но с другой стороны параметры передаются не как один массив, а метод с переменным числом параметров, итого сейчас правильно так:
тут мои добавления: listUrl -- веб-страница со списком элементов (например, новости), itemUrl -- урл самого добавляемого элемента, atomUrl -- url atom-feed. Вроде это все соответствует документации этого weblog.
Я конечно извиняюсь, а что, если сделать вот так, поисковики разве будут индексировать хвост крошек:
Я так и делаю:
Спасибо, хорошая статья научился добавлять мета теги key и desc к своим постам.
Можете еще добавить пагинацию с rel="prev" и rel="next":
Хотел спросить по поводу Ping Behavior. В геттере getUrl вызывается createUrl без второго параметра, указывающего на полный путь, т.е. создается относительный путь. RPC примет разве такой урл?)
Все, извиняюсь, вижу что в компоненте добавляется full url. Не думаю что это правильно, но да ладно.
Мне кажется что пинговать надо в отдельном потоке, например в Gearman.
А как что-то подобное реализjвать на YII2?
Добрый день.
подскажите пожалуйста как подключить IXR_Library в YII2 !!!
вроде подключаю так
use frontend\helpers\IXR_Library\IXR_Client;
но результат нолевой.
Что значит "нулевой"?
делаю так
результат
возможно я не рпавильно подключаю библиотеку??
как я сделал: из фала IXR_Library.php я вынес все классы в разные файлы.
просто если делать не на фреимворке , то результат есть.
А конструктор в классе не переопределяли?
нет. я только конструкторы привл к виду php 5.3
заменил название класса на __constructor
а не подскажите по какому принципу переопределить?
все. вроде сделал. ошибок нет. спасибо.
А на вторую версию не будет похожей статьи? Меня вот интересует как сделать так чтобы meta-данные автоматом заполнялись из таблицы pages, а если есть контроллер, то данные брались из контроллера.
Для второго регистрацию метатегов можно и так сделать в контроллере страницы:
Здравствуйте!
При использовании первого рецепта вместо сайта отображается содержимое PageHelper.
Догадываюсь, что примитивный вопрос, но как это поправить?
Спасибо!
Добавить
в начало файла.
Переделал, теперь устойчиво:
Error 500
Undefined variable: model
Найти представление view.php и найти там $model.
Спасибо, это я поправил.