Temas etiquetados como: ‘cakephp’

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'));

Autocompletado de código CakePHP en Zend Studio

7 Marzo, 2009

Para programar PHP me gusta usar Zend Studio. He probado diferentes alternativas y esta es la que más me ha convencido. Pero tiene una pega, y es que cuando uso cakePHP como framework no consigo todo el autocompletado de código que quisiera. Hasta ahora.

Cuando usamos un Model o un Component en un Controller, como se cargan dinámicamente, Zend Studio no reconoce sus métodos. La solución que he encontrado ha sido crear un atributo por cada modelo en la clase AppController y mediante el uso de documentación phpDoc indicar el tipo del atributo. Por ejemplo, supongamos que tengo un modelo llamado Post:

/**
 * @var AppModel
 */
public $Post;

Zend Studio interpreta que $Post es de tipo AppModel por lo que ya disponemos de los métodos de AppModel que Post hereda. Tiene una pega, y es que si creamos métodos propios en nuestro modelo esto no nos servirá.
Para tener autocompletado  de código en componentes es prácticamente igual:

/**
 * @var Component
 */
public $RequestHandler;

¿Y qué hay de los Helpers? En el archivo /app/config/bootstrap.php incluimos el siguiente código:

if(false) {
	$ajax = new AjaxHelper();
	$form = new FormHelper();
	$html = new HtmlHelper();
	$javascript = new JavascriptHelper();
	$number = new NumberHelper();
	$session = new SessionHelper();
	$text = new TextHelper();
	$time = new TimeHelper();
	$pagination = new PaginationHelper();
	$rss = new RssHelper();
	$xml = new XmlHelper();
	$number = new NumberHelper();
}

Como se ve en el código, por cada Helper creamos un nuevo objeto. El truco está en que esto está dentro de un if en el que nunca se entrará por lo que no afecta a nuestro código, sin embargo Zend Studio lo interpreta.
Podríamos incluirlo en cualquier archivo, pero cakePHP reserva específicamente el archivo bootstrap.php para que metamos ahí lo que queramos.

No lo he probado, pero supongo que este proceso sirve igualmente para Eclipse PDT.