Temas etiquetados como: ‘seo’

Cakephp: paginación y SEO

24 febrero, 2010

En un proyecto nuevo que estoy desarrollando me he dado cuenta que hay un problema con la paginación en CakePHP y la posibilidad de generar contenido duplicado de cara a los buscadores.

Cuando paginamos unos resultados en la URL se añade el parámetro page que nos indica el número de página de resultados que veremos. El problema surge cuando hay un enlace a la página 1 de los resultados cuyo contenido es el mismo que cuando no indicamos ese parámetro.
Veamoslo con un ejemplo. Supongamos que mi web es www.example.com y que en la página de inicio tenemos un listado de noticias, además, usando el potencial de Router de CakePHP consigo URLs del tipo www.example.com/p-2 para mostrar la segunda página de noticias. Cuando estemos en la página número dos, el paginador mostrará un enlace a la página número 1 de la forma www.example.com/p-1 cuyo contenido es exactamente el mismo que www.example.com. Resultado: los buscadores penalizan por considerar contenido duplicado.

Solución. He creado un Helper que extiende de PaginatorHelper y que modifica este comportamiento. El nuevo Helper admite un valor en el parámetro options que permite hacer que en el pagindor la primera o la última página no lleven el parámetro page en la URL. El nuevo valor de options se llama no_page y admite como valores first y last. Con first indicamos que los enlaces del paginador a la primera página no llevarán el parámetro page. Con last hacemos lo mismo pero para la última página del paginador.
Usar la opción no_page = “last” tiene sentido si desde nuestro Controller hacemos que la página que se muestre por defecto en nuestra paginación sea la última de los resultados. Es decir, si tenemos 3 páginas de comentarios, que nos muestre por defecto la página 3, donde se verán los últimos comentarios añadidos.

Código. Este es el código del Helper que he creado. Hay que incluirlo en el archivo /app/views/helpers/pager.php

<?php
App::import('Helper', 'Paginator');
class PagerHelper extends PaginatorHelper
{
 
 
    public function numbers($options = array())
    {
        if($options === true)
        {
            $options = array('before' => ' | ', 'after' => ' | ', 'first' => 'first', 'last' => 'last');
        }
 
        $defaults = array('tag' => 'span', 'before' => null, 'after' => null, 'model' => $this->defaultModel(), 'modulus' => '8', 'separator' => ' | ', 'first' => null, 'last' => null, 'no_page' => 'first');
        $options += $defaults;
 
        $params = (array) $this->params($options['model']) + array('page' => 1);
        unset($options['model']);
 
        if($params['pageCount'] <= 1)
        {
            return false;
        }
 
        extract($options);
        unset($options['tag'], $options['before'], $options['after'], $options['model'], $options['modulus'], $options['separator'], $options['first'], $options['last'], $options['no_page']);
        $out = '';
 
        if($modulus && $params['pageCount'] > $modulus)
        {
            $half = intval($modulus / 2);
            $end = $params['page'] + $half;
 
            if($end > $params['pageCount'])
            {
                $end = $params['pageCount'];
            }
            $start = $params['page'] - ($modulus - ($end - $params['page']));
            if($start <= 1)
            {
                $start = 1;
                $end = $params['page'] + ($modulus - $params['page']) + 1;
            }
 
            if($first && $start > 1)
            {
                $offset = ($start <= (int) $first) ? $start - 1 : $first;
                if($offset < $start - 1)
                {
                    $out .= $this->first($offset, array('tag' => $tag, 'separator' => $separator));
                }
                else
                {
                    $out .= $this->first($offset, array('tag' => $tag, 'after' => $separator, 'separator' => $separator));
                }
            }
 
            $out .= $before;
 
            for($i = $start; $i < $params['page']; $i++)
            {
                if(($no_page == 'first' and $i == 1)) $out .= $this->Html->tag($tag, $this->link($i, array('page' => null), $options)) . $separator;
                else $out .= $this->Html->tag($tag, $this->link($i, array('page' => $i), $options)) . $separator;
            }
 
            $out .= $this->Html->tag($tag, $params['page'], array('class' => 'current'));
            if($i != $params['pageCount'])
            {
                $out .= $separator;
            }
 
            $start = $params['page'] + 1;
            for($i = $start; $i < $end; $i++)
            {
                $out .= $this->Html->tag($tag, $this->link($i, array('page' => $i), $options)) . $separator;
            }
 
            if($end != $params['page'])
            {
                if($no_page == 'last') $out .= $this->Html->tag($tag, $this->link($i, array('page' => null), $options)) . $separator;
                else $out .= $this->Html->tag($tag, $this->link($i, array('page' => $i), $options)) . $separator;
            }
 
            $out .= $after;
 
            if($last && $end < $params['pageCount'])
            {
                $offset = ($params['pageCount'] < $end + (int) $last) ? $params['pageCount'] - $end : $last;
                if($offset <= $last && $params['pageCount'] - $end > $offset)
                {
                    $out .= $this->last($offset, array('tag' => $tag, 'separator' => $separator, 'no_page' => $no_page));
                }
                else
                {
                    $out .= $this->last($offset, array('tag' => $tag, 'before' => $separator, 'separator' => $separator, 'no_page' => $no_page));
                }
            }
 
        }
        else
        {
            $out .= $before;
 
            for($i = 1; $i <= $params['pageCount']; $i++)
            {
                if($i == $params['page'])
                {
                    $out .= $this->Html->tag($tag, $i, array('class' => 'current'));
                }
                else
                {
                    if(($no_page == 'first' and $i == 1) or ($no_page == 'last' and $i == $params['pageCount']))
                    {
 
                        $out .= $this->Html->tag($tag, $this->link($i, array('page' => null), $options));
                    }
                    else
                    {
                        $out .= $this->Html->tag($tag, $this->link($i, array('page' => $i), $options));
                    }
                }
                if($i != $params['pageCount'])
                {
                    $out .= $separator;
                }
            }
 
            $out .= $after;
        }
 
        return $out;
    }
 
 
    public function __pagingLink($which, $title = null, $options = array(), $disabledTitle = null, $disabledOptions = array())
    {
        $check = 'has' . $which;
        $_defaults = array('url' => array(), 'step' => 1, 'escape' => true, 'model' => null, 'tag' => 'span', 'class' => strtolower($which), 'no_page' => 'first');
        $options = array_merge($_defaults, (array) $options);
        $paging = $this->params($options['model']);
        if(empty($disabledOptions))
        {
            $disabledOptions = $options;
        }
 
        if(!$this->{$check}($options['model']) && (!empty($disabledTitle) || !empty($disabledOptions)))
        {
            if(!empty($disabledTitle) && $disabledTitle !== true)
            {
                $title = $disabledTitle;
            }
            $options = array_merge($_defaults, (array) $disabledOptions);
        }
        elseif(!$this->{$check}($options['model']))
        {
            return null;
        }
 
        foreach(array_keys($_defaults) as $key)
        {
            ${$key} = $options[$key];
            unset($options[$key]);
        }
        $url = array_merge(array('page' => $paging['page'] + ($which == 'Prev' ? $step * -1 : $step)), $url);
 
        if(($no_page == 'first' and $url['page'] == 1) or ($no_page == 'last' and $url['page'] == $paging['pageCount'])) $url['page'] = null;
 
        if($this->{$check}($model))
        {
            return $this->link($title, $url, array_merge($options, compact('escape', 'class')));
        }
        else
        {
            return $this->Html->tag($tag, $title, array_merge($options, compact('escape', 'class')));
        }
    }
 
 
    public function first($first = '<< first', $options = array())
    {
        $options = array_merge(array('tag' => 'span', 'after' => null, 'model' => $this->defaultModel(), 'separator' => ' | ', 'no_page' => 'first'), (array) $options);
 
        $params = array_merge(array('page' => 1), (array) $this->params($options['model']));
        unset($options['model']);
 
        if($params['pageCount'] <= 1)
        {
            return false;
        }
        extract($options);
        unset($options['tag'], $options['after'], $options['model'], $options['separator'], $options['no_page']);
 
        $out = '';
 
        if(is_int($first) && $params['page'] > $first)
        {
            if($after === null)
            {
                $after = '...';
            }
            for($i = 1; $i <= $first; $i++)
            {
                if($no_page == 'first' and $i == 1) $out .= $this->Html->tag($tag, $this->link($i, array('page' => null), $options));
                else $out .= $this->Html->tag($tag, $this->link($i, array('page' => $i), $options));
 
                if($i != $first)
                {
                    $out .= $separator;
                }
            }
            $out .= $after;
        }
        elseif($params['page'] > 1)
        {
            $out = $this->Html->tag($tag, $this->link($first, array('page' => 1), $options)) . $after;
        }
        return $out;
    }
 
 
    public function last($last = 'last >>', $options = array())
    {
 
        $options = array_merge(array('tag' => 'span', 'before' => null, 'model' => $this->defaultModel(), 'separator' => ' | ', 'no_page' => 'first'), (array) $options);
 
        $params = array_merge(array('page' => 1), (array) $this->params($options['model']));
        unset($options['model']);
 
        if($params['pageCount'] <= 1)
        {
            return false;
        }
 
        extract($options);
        unset($options['tag'], $options['before'], $options['model'], $options['separator'], $options['no_page']);
 
        $out = '';
        $lower = $params['pageCount'] - $last + 1;
 
        if(is_int($last) && $params['page'] < $lower)
        {
            if($before === null)
            {
                $before = '...';
            }
            for($i = $lower; $i <= $params['pageCount']; $i++)
            {
                if($no_page == 'last' and $i == $lower) $out .= $this->Html->tag($tag, $this->link($i, array('page' => null), $options));
                else $out .= $this->Html->tag($tag, $this->link($i, array('page' => $i), $options));
 
                if($i != $params['pageCount'])
                {
                    $out .= $separator;
                }
            }
            $out = $before . $out;
        }
        elseif($params['page'] < $params['pageCount'])
        {
            $out = $before . $this->Html->tag($tag, $this->link($last, array('page' => $params['pageCount']), $options));
        }
        return $out;
    }
}
?>

Este es un ejemplo de cómo se usaría el nuevo Helper.

echo $this->Pager->prev('« Anterior', array('class' => 'nextprev','no_page' => 'first'), '« Anterior', array('class' => 'nextprev', 'tag' => 'span'));
echo $this->Pager->numbers(array('separator' => '', 'modulus' => 5, 'no_page' => 'first', 'first' => 1, 'last' => 1));
echo $this->Pager->next('Siguiente »', array('class' => 'nextprev','no_page' => 'first'), 'Siguiente »', array('class' => 'nextprev', 'tag' => 'span'));