Навигация по автометкам

Хочу поделиться своим способом работы с метками, для дополнительной навигации по ресурсам. Вообще то, для этого уже есть tagLister, но мне нравится контролировать процесс. И вообще, это мой первый пост тут, так что сильно не пинайте :)




Для создания, собственно меток, логично использовать штатный tv параметр с типом autotag (автометка), поэтому вначале, создаю его и как нибудь называю. Не задумываясь особенно, назвал его по названию типа — autotag. Добавляю созданный tv в шаблон для вывода постов и с ним — все.





Для вывода меток, делаю два сниппета, один для создания облака меток — tagCloud, второй для вывода меток в блоге и в самом посте — tagLinks.

tagCloud:
<?php
$base = $modx->config['base_url'];
$tvname = $modx->getOption('tvname', $scriptProperties, "autotag");
$output = "";

$content_type = $modx->getObject('modContentType', array('mime_type' => 'text/html'));
if(substr_count($_SERVER["REQUEST_URI"], $content_type->get('file_extensions'))) {
	$parent = $modx->resource->parent;
	$url = $modx->makeUrl($modx->resource->parent);
}else{
	$parent = $modx->resource->id;
	$url = $modx->resource->uri;
}

$q = $modx->newQuery('modTemplateVarResource');
$q->select('DISTINCT(`modTemplateVarResource`.`value`)');
$q->innerJoin('modTemplateVar', 'tv', "tv.id = modTemplateVarResource.tmplvarid");
$q->innerJoin('modResource', 'res', 'res.id=modTemplateVarResource.contentid');
$q->where(array(
		'tv.name' => $tvname, 
		'res.context_key' => $modx->resource->context_key,
		'res.parent' => $parent
	)
);

if($q->prepare() && $q->stmt->execute()) {
	while ($row = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
		$result[] = $row['value'];
	}
}
if($result) {
    $result = implode($result, ',');
    $result = array_unique(explode(',', $result));
    
    foreach($result as $value) {
    	$output .= "<a href='{$base}{$url}?tag={$value}'>{$value}</a> ";
    }
}
return $output;

Этот сниппет выводит список уникальных значений меток, в виде ссылок, с GET параметром, который потом будет использоваться, для фильтрации вывода ресурсов:
[[!tagCloud:default=`Пока нет меток`? &tvname=`autotag`]]

<a href="/path/?tag=метка">метка</a>
<a href="/path/?tag=опятьметка">опятьметка</a>
<a href="/path/?tag=ещеметка">ещеметка</a>

Что в нем происходит:
Из таблицы с классом «modTemplateVarResource», выбираю все tv с типом «autotag» и именем «autotag», связанные с ресурсами находящимися в текущем контексте, родителем для которых, в случае вывода в блоге, является ресурс где вызывается сниппет, а в случае вывода в статье — родитель статьи. Так как в одной автометке, может быть много значений, перечисленных через запятую, то сначала, на уровне запроса, убираю повторяющиеся множественные значения, затем разбиваю результирующий массив по запятым и из него, так же, убираю повторяющиеся значения. В конце формирую ссылки.

tagLinks:
<?php
$tags = $modx->getOption('tags', $scriptProperties);
$get = $modx->getOption('get', $scriptProperties, '0');
$base = $modx->config['base_url'];

$content_type = $modx->getObject('modContentType', array('mime_type' => 'text/html'));
if(substr_count($_SERVER["REQUEST_URI"], $content_type->get('file_extensions'))) {
	$url = $modx->makeUrl($modx->resource->parent);
}else{
	$url = $modx->resource->uri;
}

if(!$get){
	if(!$tags) {return '';}
	$tags = explode(',',$tags);
	foreach($tags as $value) {
		$output[] = "<a href='{$base}{$url}?tag={$value}'>{$value}</a>"; 
	} 

	return implode(' ',$output);
}else{
	return (!empty($_GET['tag']))? "autotag==%{$_GET['tag']}%" : '';
}

Этот сниппет, выводит список ссылок, для блога и ресурса, а так же передает GET-параметр в вызов pdoResources, для фильтрации по меткам. В принципе, это разные задачи, но я решил не плодить сниппеты.

Что в нем происходит:
Сниппет может принимать в качестве параметров два значения:
[[!tagLinks? &tags=`[[*autotag]]`]] 
и
[[!tagLinks? &get=`1`]]

В первом случае, сниппет разбивает полученную автометку на отдельные значения, заворачивает их в ссылки, как в tagCloud и выводит. Для того, чтобы можно было выводить метки и в блоге и в самой статье, в начале сниппета получаю расширение для типа html и проверяю его наличие в адресной строке. Если адрес заканчивается на .html, то в url попадает адрес родителя (для статьи).







Во втором случае, когда передается параметр &get=`1`, сниппет смотрит в адресную строку и если там есть значение метки, то передает его в вывод, оформляя подходящим для фильтрации образом.

А вот вызов pdoResources, для создания блога и фильтрации по автометке.
[[!pdoResources? 
	&parents=`[[*id]]`
	&tvFilters=`[[!tagLinks? &get=`1`]]`
	&tpl=`@INLINE <div class="row">
                    <div class="cols col-10 intro">
                        <h2><a href="{{+link}}">{{+pagetitle}}</a> ({{+publishedon}})</h2>
                        <div>{{!tagLinks? &tags=`{{+tv.autotag}}`}}</div>
                        {{+introtext}} <a href="{{+link}}">читать дальше..</a>
                    </div>
                </div>`	
]]

На уникальность не претендую :)

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

tagRelated:
<?php
$tpl = $modx->getOption('tpl', $scriptProperties);
$limit = $modx->getOption('limit', $scriptProperties, 5);
$tvname = $modx->getOption('tvname', $scriptProperties, "autotag");
$tvvalue = $modx->resource->getTVValue($tvname);
$base = $base = $modx->config['base_url'];
$currentid = $modx->resource->id;
$output = '';

$q = $modx->newQuery('modResource', array(
	'context_key' => $modx->resource->context_key,
	'parent' => $modx->resource->parent
));
$q->select('pagetitle,introtext,publishedon,uri,tvres.value as autotag');
$q->innerJoin('modTemplateVarResource', 'tvres', "tvres.contentid = modResource.id");
$q->innerJoin('modTemplateVar', 'tv', "tv.id = tvres.tmplvarid");
$q->limit($limit);
$q->where(array(
		'modResource.id:!=' => $currentid,
		'tv.name' => $tvname,
		'tvres.value:REGEXP' => str_replace( ',', '|', $tvvalue)
	)
);

if($q->prepare() && $q->stmt->execute()) {
	while ($row = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
		$output .= $modx->getChunk($tpl, array(
			'url' => $base.$row['uri'], 
			'date' => date("Y-m-d H:i:s", $row['publishedon']),
			'pagetitle' => $row['pagetitle'],
			'text' => $row['introtext']
		));
	}
}

return $output;

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

Вызов:
[[!tagRelated:default=`Пока нет связанных постов`?
	 &tpl=`RELATED`
	 &limit=`4`
	 &tvname=`autotag`
]]

Чанк RELATED:
<div>
	<a href="[[+url]]">[[+pagetitle]]</a> <span class="date">[[+date]]</span><br />
	<div class="sidetext">[[+text:notags:ellipsis=`100`]]</div>
</div>
Саша Туманов
16 апреля 2015, 18:58
modx.pro
28
6 914
+10

Комментарии: 23

Klike
17 апреля 2015, 08:57
0
Потрясающе! Пару дней искал решение, и тут ИДЕАЛЬНО! Прямо то, что нужно. Спасибо!
Один вопрос, можно ли указать контейнер ресурсов, чтобы на странице со статьёй тоже облако тегов отображалось?
    Саша Туманов
    17 апреля 2015, 11:39
    0
    Поправил tagCloud, для вывода в статье тоже.
      Klike
      17 апреля 2015, 11:49
      0
      Спасибо! И ещё малюсенький вопрос. На сайте есть метки и категории, по сути те же метки. Как добавить в tagLinks, чтобы по двум тв параметрам сравнивал и выводил подходящие. Сделал два сниппета, на каждый тв. В вызове pdoResources указал в tvFilters
      &tvFilters=`[[!tagLinks? &get=`1`]] || [[!caregoryLinks? &get=`1`]]`
      В таком случае все отлично фильтрует, но тогда чистый блог не выводит совсем.
        Klike
        17 апреля 2015, 13:44
        0
        Наверное лучше так:
        return (!empty($_GET['tag']))? "tags1==%{$_GET['tag']}%||tags2==%{$_GET['tag']}%" : '';
        Klike
        17 апреля 2015, 12:16
        0
        И ещё один момент: если расширения нет к html
        Расширение html, как и домен www являются ненужными призраками прошлого — можно смело от них избавляться. ©bezumkin.ru

        Тоже не показывает в статьях, как и tagLinks, если закомментировать несколько строчек, то всё ок.
        $content_type = $modx->getObject('modContentType', array('mime_type' => 'text/html'));
        // if(substr_count($_SERVER["REQUEST_URI"], $content_type->get('file_extensions'))) {
        	$url = $modx->makeUrl($modx->resource->parent);
        /* }else{
        	$url = $modx->resource->uri;
        }
        */
          Klike
          17 апреля 2015, 12:50
          0
          Может лучше делать проверку по наличию .html в строке. А, к примеру, указывать в вызове &parent, а в снимете сравнивать уже:

          кусок tagLinks
          $parent = $modx->getOption('parent', $scriptProperties, $modx->resource->parent);
          if($parent == $modx->resource->parent) {
          	$url = $modx->makeUrl($modx->resource->parent);
          }else{
          	$parent = $modx->resource->id;
          	$url = $modx->resource->uri;
          }
          И аналогично у tagCloud:
          $parent = $modx->getOption('parent', $scriptProperties, $modx->resource->parent);
          if($parent == $modx->resource->parent) {
          	$url = $modx->makeUrl($modx->resource->parent);
          }else{
          	$parent = $modx->resource->id;
          	$url = $modx->resource->uri;
          }
            Саша Туманов
            17 апреля 2015, 21:51
            0
            Тим, это ведь не компонент с инсталлятором, где нужно сразу учитывать все потенциальные нужды. Я думаю, что тут главное хорошо составленный запрос, а танцы вокруг него, выполняются по произвольной программе ;)
              Klike
              18 апреля 2015, 09:10
              0
              Да, верно) В любом случае спасибо огромное за решение! А дорабатывает под свои нужды пусть каждый сам ))
            Михаил
            11 февраля 2016, 10:39
            0
            Саша, спасибо за интересный материал!

            Для мультиязычности Babel используется?
            Настройки как по мануалу?
              Саша Туманов
              17 февраля 2016, 19:50
              +1
              Да, Babel. Делал, в основном, по Васиной инструкции.

              Плагин переключающий контексты:
              <?php
              // Работаем только на фронтенде и только с friendly urls
              if ($modx->event->name != 'OnHandleRequest' || $modx->context->key == 'mgr' || !$modx->getOption('friendly_urls')) {return;}
              
              // Получаем запрашиваемый url
              $alias = $modx->getOption('request_param_alias', null, 'alias', true);
              $request = &$_REQUEST[$alias];
              
              // Выбираем контексты с настройкой base_url
              $q = $modx->newQuery('modContextSetting', array('key' => 'base_url', 'value:!=' => ''));
              $q->select('context_key,value');
              
              $contexts = array();
              $tstart = microtime(true);
              if ($q->prepare() && $q->stmt->execute()) {
              	// Учитываем наш запрос в БД
              	$modx->queryTime += microtime(true) - $tstart;
              	$modx->executedQueries++;
              	// Разбираем результаты
              	while ($row = $q->stmt->fetch(PDO::FETCH_ASSOC)) {
              		$base_url = trim($row['value'], '/');
              		$context = $row['context_key'];
              		// Если запрос начинается с base_url какого-то контекста
              		if (preg_match('/^('.$base_url.')\//i', $request)) {
              			// То переключаемся на этот контекст
              			// Web инициализируется в index.php - на него переключаться не нужно
              			if ($context != 'web') {
              				$modx->switchContext($context);
              			}
              			// Вырезаем base_url из запроса, чтобы MODX нашел ресурс по uri
              			$request = preg_replace('/^'.$base_url.'\//', '', $request);
              			// Дело сделано - выходим из цикла
              			break;
              		}
              	}
              }

              Сниппет переключающий языки:
              <?php
              //error_reporting(E_ALL | E_STRICT);
              //ini_set('display_errors', 1);
              $class = $modx->getOption('class', $scriptProperties, '');
              $divider = PHP_EOL;
              $output = '';
              
              function getLink($context = 'web') {
                  global $modx;
              	if ($modx->getOption('site_start') != $modx->resource->id && $modx->getCount('modResource', array('uri' => $modx->resource->uri, 'context_key' => $context))) {
              		return $modx->resource->uri;
              	}
              	return '';
              }
              
              switch ($modx->context->key) {
                  case 'en':
                  	$output .= "<a class='".$class."' href='/".getLink('web')."'>RUS</a> {$divider}
                  	            <a class='".$class."' href='kz/".getLink('kz')."'>KAZ</a>
                  	            <span class='".$class."'>ENG</span> {$divider}";
                      break;
                  case 'kz':
                  	$output .= "<a class='".$class."' href='/".getLink('web')."'>RUS</a> {$divider}
                  	            <span class='".$class."'>KAZ</span>
                  	            <a class='".$class."' href='en/".getLink('en')."'>ENG</a> {$divider}";
                      break;
                  default:
                  	$output .= "<span class='".$class."'>RUS</span> {$divider}
                  	            <a class='".$class."' href='kz/".getLink('kz')."'>KAZ</a>
                  	            <a class='".$class."' href='en/".getLink('web')."'>ENG</a> {$divider}";
              }
              
              return $output;

              Настройки контекстов:
              en
              base_url				base_url	/en/
              Язык					cultureKey	en
              http_host				http_host	rirme.kz
              Главная страница сайта	site_start	2
              site_url				site_url	http://rirme.kz/
              
              kz
              base_url				base_url	/kz/
              Язык					cultureKey	kz
              http_host				http_host	rirme.kz
              Главная страница сайта	site_start	3
              site_url				site_url	http://rirme.kz/
              
              web
              base_url				base_url	/
              Язык					cultureKey	ru
              http_host				http_host	rirme.kz
              Главная страница сайта	site_start	1
              site_url				site_url	http://rirme.kz/

              Настройки системы -> Babel
              babel.contextKeys		babel.contextKeys	web,en,kz

              Тег base в начале странички:
              <base href="[[++site_url]]" />
                Михаил
                17 февраля 2016, 19:58
                0
                Спасибо огромное за развернутый ответ!
          yani
          17 апреля 2015, 09:00
          0
          Полезно, мне как то достался сайт, где было 10-15 ТВшек и все они были с типом АВТОМЕТКА, со временем страничка ресурса в админке перестала грузиться и пришлось убрать там этот тип(
          Но повторюсь, кол-во таких ТВ было большое
            Владимир
            05 мая 2015, 11:31
            0
            В соц сети очень полезно добавлять хэш тэги, типа #тэг1 и т.п.
            Само собой, напрашивается: tagLinks урезаем, делаем HashtagLinks
            [[!HashtagLinks? &tags=`[[*tags]]`]]
            <?php
            $tags = $modx->getOption('tags', $scriptProperties);
            if(!$get){
            	if(!$tags) {return '';}
            	$tags = explode(',',$tags);
            	foreach($tags as $value) {
            		$output[] = "#{$value}"; 
            	} 
            
            	return implode(' ',$output);
            }
            Одно досадно, предзаполнять поле комментариев по ogp.me/ не предусмотрено, а в <meta property=«og:description» content="[[!HashtagLinks? &tags=`[[*tags]]`]] добавлять бессмысленно, что сводит все на нет, т.е. постинг хэш-тегов в комментарии вручную.
            Может у кого есть мысли как реализовать автопостинг и хэштэгов тоже?
              Алексей
              26 сентября 2015, 21:25
              0
              Помогите новичку пожалуйста.
              Я использовал раньше такой вариант taglist
              <?php
              $tags = $modx->getOption('tags', $scriptProperties);
              $get = $modx->getOption('get', $scriptProperties, '0');
              $base = $modx->config['base_url'];
              $content_type = $modx->getObject('modContentType', array('mime_type' => 'text/html'));
              $url = $modx->makeUrl($modx->resource->parent);
              if(!$get){
              	if(!$tags) {return '';}
              	$tags = explode(',',$tags);
              	foreach($tags as $value) {
              		$output[] = "<a class='tag' href='/tags?tag={$value}' rel=\"nofollow\">{$value}</a>"; 
              	}
              	return implode(' ',$output);
              }else{
              	return (!empty($_GET['tag']))? "tags==%{$_GET['tag']}%" : '';
              }
              Вызывал pdopage и в чанке прописывал так:
              <p>Тэги: [[!tagLinks? &tags=`[[+tv.tags]]`]]</p>
              Все прекрасно работало, но вот сейчас перенес все тэги из TV в поле description.
              Надеялся что замена
              $tags = $modx->getOption('tags', $scriptProperties);
              на
              $tags = $modx->resource->get('description');
              поможет, но в чанк
              <p>Тэги: [[!tagLinks? &tags=`[[+description]]`]]</p>
              передается только дескрипшен той странице на которой вызывается, а не того ресурса который передается из pdopage.
              Подскажите как быть?
                Виктор Лобанов
                11 февраля 2016, 01:26
                0
                Подскажите пожалуйста, а как вывести теги не через pdoResources а если статьи выводятся через getPage?
                  Воеводский Михаил
                  11 февраля 2016, 01:36
                  0
                  getPage сам по себе ничего не выводит, он только обеспечивает разбиение на страницы. Выводом, скорее всего, занимается getResources. В таком случае самым простым вариантов будет установка pdoTools и замена в вызове [[getPage]] на [[pdoPage]]. А дальше — по многочисленным здесь руководствам и ответам.
                Igorevich
                20 февраля 2016, 19:43
                +1
                Спасибо интересное решение, возник вопрос, можно ли как-то страницам с тегами задавать свой title и description, т.е например пользователь кликает на тег и попадает на страницу мойсайт/tag где выводятся посты с текущим тегом и на этой странице был свой заголовок и описание для ПС.
                  Борис И
                  21 февраля 2016, 09:25
                  0
                  Тоже думаю на эту тему, решения не нашел. Есть идея, создать отдельный раздел (секцию) — TEG, там будут ресурсы, по одному ресурсу для вывода статей с этим тегом. Так можно прописывать title и description.
                  Вывод осуществлять с помощью pdoPage с фильтрацией &tvFilters=`тег`.
                  Облако тегов будет — вывод всех ресурсов из созданного раздела — TEG.
                  Встанет проблема как вывести теги (ссылки) принадлежащие одной статье. Можно использовать taglister, но он будет формировать свои ссылки, которые нам не нужны. Можно попробовать подменять их с помощью компонента redirector, направлять на наши созданные ресурсы, вместо ссылок taglister.
                  Сам пока не реализовывал, но другого варианта пока не нашел. А title и description для страниц вывода материалов по тегам, вещь нужная и полезная, можно получить дополнительных посетителей, поисковики сейчас любят когда у вас подборка материалов по теме, раскрывающая ее (но правильное оформление страницы — обязательно).
                  Минус решения — трудоемко и страницы, с тегами придется создавать ручками. Как бы автоматизировать…
                  DOM
                  DOM
                  30 марта 2016, 10:06
                  0
                  Добрый день. Интересует вопрос. А можно как-то сделать чтобы авто-метки подтягивались для каждого языка только свои. Мультиязычность реализована контекстами через Babel?
                    Саша Туманов
                    09 сентября 2019, 07:35
                    0
                    Вот, вдруг кому-нибудь всё еще интересно. Создаю три tv для каждого контекста и показываю каждый только в соответствующем контексте:

                    if($modx->event->name == 'OnDocFormRender') {
                    	$tvs = [
                    		"tags_web",
                    		"tags_ua",
                    		"tags_en"
                    	];
                    	$context = $resource->get('context_key');
                    	$tohide = "";
                    
                    	foreach ($tvs as $tv) {
                    		if (! $res = $modx->getObject('modTemplateVar', array('name' => $tv)))
                    			continue;
                    		$tv_id = $res->get("id");
                    		$tv_ctx = end(explode("_", $tv));
                    		if ($tv_ctx == $context) 
                    			continue;
                    
                    		$tohide .= "'tv".$tv_id."',";
                    	}
                    	$tohide = rtrim($tohide, ',');
                            
                            // Такие функции смотреть в "manager/assets/modext/core/modx.js"
                    	$hideTVs = "MODx.hideTVs([{$tohide}]);";
                    	$modx->regClientStartupHTMLBlock("<script>
                    			Ext.onReady(function(){
                    				$hideTVs
                    			});
                    		</script>");
                    }

                    Ну и предыдущую логику тоже придется немного переделать.
                    Анатолий
                    02 апреля 2020, 09:41
                    0
                    День добрый, Александр.
                    Посмотреть демо или скачать бекап демо есть возможность??
                    Не все получилось.
                    Хотелось бы до конца разобраться.
                    Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
                    23