Нужна ли HTML карта сайта или нет — это науке не известно. Как минимум она нужна SEO — специалистам. :)
В простейшем случае, карта сайта — это портянка урлов всех публикаций, т.н. плоский список. Иерархическая карта (в виде дерева) обычно базирует свою структуру на каком-либо меню сайта. Для того и другого случая можно взять готовые модули.
А я расскажу как написать свой модуль карты сайта.
Бывают частные случаи, которые готовыми модулями не могут быть решены. Типична задача с каталогом товарных позиций. Если товаров много, то их не добавляют в меню. Но по логике товары требуется монтировать в определенный раздел сайта, так чтобы это выглядело как будто товар находится вот здесь:
[главная] — [каталог товаров] — [рубрика] — [товарная позиция]
Возможно, что вместо рубрики у вас целое дерево из рубрик и подрубрик.
Встречается и более простые примеры, например, лента новостей, блог. Здесь для публикаций также не создают пункта в меню, но есть какой то раздел, в который по логике они должны быть смонтированы.
[главная] — [все новости] — [новостная публикация]
Концепция карты сайта
Кроме публикаций, у вас наверняка на сайте могут быть разделы, созданные программно. Нам нужно объединить на карте оба множества — публикации и программные разделы.
Все необходимые для карты программные разделы нужно включить в это меню. Мы его возьмем за основу дерева, в которое будем монтировать публикации не вошедшие в меню.
Раздел sitemap.html
Нам понадобится какой то раздел для карты сайта. Я назвал модуль SM (sitemap).
1 2 3 4 5 6 7 8 9 10 |
/* hook menu */ function sm_menu() { $items['sitemap.html'] = array( 'title' => 'Карта сайта', 'page callback' => '_sm_sitemap', 'access arguments' => array('access content'), 'type' => MENU_NORMAL_ITEM, ); return $items; } |
Отсечь все лишнее
Карту придется создавать в несколько этапов.
Сначала мы получим необходимые данные о публикациях сайта — это название, url, синоним адреса и тип публикации. Весь этот список мы будет монтировать в заготовку карты, которой будет служить main-menu.
Не все типы публикаций, возможно, попадут на карту, так что составьте свой список типов для запроса node.type in (‘page’, ….) .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* html карта сайта */ function _sm_sitemap() { //все материалы сайта $res = db_query(" SELECT node.nid, node.title, node.type, UA.alias, CONCAT('node/', node.nid) as path FROM node LEFT JOIN url_alias UA ON UA.source = CONCAT('node/', node.nid) WHERE node.type in ('page', ....) AND node.status <> 0"); $front = variable_get('site_frontpage', 'node'); //массив публикаций для карты $pub = array(); while ($r = $res->fetchObject()) { if ($r->path == $front) $r->path = '<front>'; $pub[$r->path] = $r; } //меню, как заготовка для дерева $tree = menu_tree_all_data('main-menu'); |
Определим, куда будем монтировать публикации. Я объявляю перечень точек монтирования, к примеру публикации article я монтирую по адресу /blog. Далее следует три этапа монтирования и рендеринг карты.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$mount_points = array( //раздел, куда монтировать - тип публикации 'blog' => array('type' => 'article'), 'all-goods-catalog' => array('type' => 'food') ... ); //1-й проход - анализ товаров, монтирование в каталог, //поиск точек монтирования в меню _sm_sitemap_first_scan($tree, $pub, $mount_points); //2-й проход - монтирование публикаций в точки монтирования _sm_sitemap_mounts($tree, $pub, $mount_points); //3-й проход - монтирование оставщихся публикаций в корень _sm_sitemap_rootsmount($tree, $pub); //4-й проход - это рендеринг дерева return _sm_sitemap_rendering($tree); } |
До рендеринга требуется определить заголовок и ссылку каждого элемента. Синонимы публикаций мы загрузили с самого начала, чтобы не подсматривать их для каждой статьи в отдельности.
Проход 1. Предварительные ласки
Здесь мы проводим анализ существующих пунктов меню, монтируем товары в каталог. Попутно ищем точки монтирования в меню для следующего прохода.
Функция рекурсивно обходит все дерево.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
function _sm_sitemap_first_scan(&$tree, &$pub, &$mount_points) { foreach ($tree as $k => &$v) { if (!empty($v['link']['proceed'])) continue; $link_path = $v['link']['link_path']; if (isset($pub[$link_path])) { //устанавливаем пути и заголовки $nd_info = &$pub[$link_path]; $v['link']['link_title'] = $nd_info->title; $v['link']['uri'] = empty($nd_info->alias) ? $link_path : $nd_info->alias; //монтирование в КАТАЛОГ //Для рубрик каталога я использую специальный тип материала catalog //Каждая публикация рубрики содержит параметры фильтрации //Так я могу определить какие товары принадлежат именно данной рубрике if ($nd_info->type == 'catalog' && empty($v['below']) && $node_cat = node_load($nd_info->nid)) { //создаем запрос на конструкторе запросов DRUPAL $query = db_select('node', 'n'); $query->fields('n', array('nid')); $query->addExpression('CONCAT(:nnn, n.nid)' , 'path', array(':nnn' => 'node/')); //здесь я применяю фильтры именно для моего случая //они вам не будут интересны :) - я их пропущу ... //выполним запрос //переменная называется КОРМА, так как товарами //у меня были корма для животных $foods = $query->execute()->fetchAll(); //монтируем найденные корма if (!empty($foods)) { foreach ($foods as $food) { if (isset($pub[$food->path])) { $nd = $pub[$food->path]; $v['below'][] = array('link' => array( 'link_title' => $nd->title, 'uri' => (empty($nd->alias) ? $nd->path : $nd->alias), //так как монтирование идет в сканируемое дерево //надо как то пометить ветки не требующие обработки 'proceed' => 1 ), 'below' => array()); unset($pub[$food->path]); } } } } //удаляем статью из списка, т.к. она уже смонтирована unset($pub[$link_path]); } else { $v['link']['uri'] = $link_path; } if ($v['link']['uri'] == '<front>') $v['link']['uri'] = ''; if (isset($mount_points[$link_path])) { //обнаружена точка монтирования в дереве $mount_points[$link_path]['leaf'] = &$tree[$k]; } //рекурсия по дереву if (!empty($v['below'])) _sm_sitemap_first_scan($v['below'], $pub, $mount_points); } } |
Проход 2
Здесь мы смонтируем все публикации в соответствующие точки монтирования. Здесь обходимся без рекурсии, но нам приходится пробежаться по всему списку публикации (за вычетом тех, что уже смонтировали).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* используем точки монтирования */ //перебираем базу публикаций и пытаемся подобрать для каждой точку монтирования function _sm_sitemap_mounts(&$tree, &$pub, $mount_points) { //проход по списку публикаций foreach($pub as $k => &$v) { //проход по точкам монтирования foreach ($mount_points as $mnt) { if ($v->type == $mnt['type']) { //монтируем $mnt['leaf']['below'][] = array('link' => array( 'link_title' => $v->title, 'uri' => (empty($v->alias) ? $v->path : $v->alias), ), 'below' => array()); //убираем публикацию из списка unset($pub[$k]); } } } } |
Проход 3
Самый простой из всех. Если у нас остались какие то публикации в списке, которые мы не учли в меню или в перечне точке монтирования, то их надо смонтировать в корень.
1 2 3 4 5 6 7 8 9 10 11 |
/* монтируем остатки в корень */ function _sm_sitemap_rootsmount(&$tree, $pub) { foreach($pub as $k => &$v) { $tree[] = array('link' => array( 'link_title' => $v->title, 'uri' => (empty($v->alias) ? $v->path : $v->alias), ), 'below' => array()); } } |
Рендеринг дерева карты сайта
Для оснастки я воспользовался неупорядоченным списком (<ul></ul>). Поправить на <div> или какой то другой тег, думаю, вам не составит труда.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* рендеринг дерева карты сайта */ function _shra_contribution_sitemap_rendering($tree) { $out = ''; foreach ($tree as $k => $v) { $out .= '<li><a href="/' . $v['link']['uri'] . '" title="' . htmlspecialchars($v['link']['link_title']) . '">' . htmlspecialchars($v['link']['link_title']) . '</a>'; //рекурсия по дереву if (!empty($v['below'])) $out .= _shra_contribution_sitemap_rendering($v['below']); $out .= '</li>'; } return '<ul class="sitemap-tree">' . $out . '</ul>'; } |
Не рассчитываю, что данный обзор окажется полезен для широкой аудитории. Я преследую цель законспектировать технику до следующего использования в очередном проекте. Но если у вас есть вопросы, я охотно на них отвечу :).