Archivo de la etiqueta: eggBlog

Guardar artículo como borrador en eggBlog

[programación]

Sigo con la ampliación de funcionalidades de eggBlog. En este caso he añadido la posibilidad de guardar borradores de artículos. Hacía tiempo que quería hacer esta modificación, sobre todo porque tiendo a escribir artículos kilométricos que tardo varios días en terminar, y en ocasiones tengo alguna idea buena para un artículo que quiero empezar a escribir y finalizar más adelante.

Pensaba que esta modificación sería sencilla, pero no lo ha sido porque hay que modificar muchas pequeñas cosas en muchos sitios distintos. Incluso ahora que estoy un par de días haciendo pruebas, sigo tenido la sensación de que me estoy dejando algo.

Las módificaciones a realizar son las siguientes:

Primero añadimos una nueva traducción a langs/es.php:

$lang['draft'] = "Borrador";

He añadido un par de funciones útiles a _lib/util.php. La primera comprueba si el usuario actual es el administración:

function userIsAdmin() {
    return !empty( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) &&
    eb_checkadmin( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] );
}

La segunda comprueba si un articulo debe ser visible para el usuario. El administrador debe tener acceso a todos los artículos, pero el resto de usuarios no deben poder ver los borradores:

function articleIsVisibleToUser( $articleId ) {
    settype( $articleId, "integer" );

    if ( userIsAdmin() ) {
        return true;
    }

    $sql = "SELECT article_flag FROM eb_articles"
    ." WHERE article_id = " . $articleId;

    if( $row = mysql_fetch_array( mysql_query( $sql ) ) ) {
        $articleFlag = $row['article_flag'];

        if( $articleFlag == 1 || $articleFlag == 2 ) {
            return true;
        }
    }

    return false;
}

En _lib/news.php tenemos un montón de cambios desperdigados. Esto se debe al nuevo estado que puede tener un artículo. Los estados, incluido el nuevo son:

  • Artículo eliminado (Es decir, los artículos no se eliminan, solo se ocultan)
  • Artículo normal
  • Artículo destacado (Aparecería al inicio de todas las páginas)
  • Borrador de artículo

Para los ficheros con muchos cambios de código, he diseñado una nueva forma para mostrar las diferencias. A ver que tal queda:

Archivo: _lib/news.php - Método: eb_articlelist($n)

$showDrafts = userIsAdmin();

$sql = "SELECT article_id, article_title, article_date, article_flag"
." FROM eb_articles WHERE article_flag = 1";
if( $showDrafts == true ){
    $sql .= " OR article_flag = 3";
}
$sql .= " ORDER BY article_date DESC";

$query = mysql_query( $sql );

Archivo: _lib/news.php – Método: eb_articlelist($n)

.eb_links_article( $row['article_title'], $row['article_id'] ) . '"';
if( $row['article_flag'] == 3 ) {
    $output .= ' style="color:red"';
}
$output .= '>' . $row['article_title'] . '</a></li>';

Archivo: _lib/news.php – Método: eb_articles($n)

$showDrafts = userIsAdmin();

$output=array();
$sql = "SELECT article_id FROM eb_articles WHERE article_flag=1";
if( $showDrafts == true ) {
    $sql .= " OR article_flag = 3";
}    
$sql .= " ORDER BY article_date DESC LIMIT " . $n;

Archivo: _lib/news.php – Método: getArticlesIdsForPage( $page, $n )

$showDrafts = userIsAdmin();

$articleIds = array();
$sql = "SELECT article_id FROM eb_articles WHERE article_flag = 1";
if( $showDrafts == true ) {
    $sql .= " OR article_flag = 3";
}
$sql .= " ORDER BY article_date DESC";

Archivo: _lib/news.php – Método: eb_article( $comments, $id )

$sql = "SELECT article_id, article_date, article_comments, article_title"
.", article_body, user_name, article_flag"
." FROM eb_articles AS a LEFT JOIN eb_users AS u ON a.author_id = u.user_id"
." WHERE article_id = ".$id." ORDER BY article_date DESC";

Archivo: _lib/news.php – Método: eb_article( $comments, $id )


$output .= " <div class="article"> ";

$draftLabel = "";
if( $row['article_flag'] == 3 ) {
    $draftLabel = "<span style="color: red;">(Borrador)</span>";
}

Archivo: _lib/news.php – Método: eb_article( $comments, $id )

$output.=" <h3>" . $draftLabel . "<a href=""
.eb_links_article( $row['article_title'], $row['article_id'] ) . "">"
.$row['article_title'] . "</a></h3>";

Archivo: _lib/news.php – Método: eb_article( $comments, $id )

if(userIsAdmin()){
    $output.=" <a href="admin.php?editarticle=" . $row[0]."">"
    .$lang['edit']." | ."">".$lang['delete']."</a>";
}

Archivo: _lib/news.php – Método: eb_article($comments,$id)

if(userIsAdmin()) {
    $output .= " <a href="admin.php?editcomment=" . $comment[3]
    ."&article=" . $row[0] . "">" . $lang['edit']
    ."</a> | <a href="admin.php?deletecomment=" . $comment[3]
    ."&article=" . $row[0] . "">" . $lang['delete'] . "</a>";
}

Archivo: _lib/news.php – Método: eb_tags($n)

$sql="SELECT tag_name, COUNT(eb_tags_links.article_id) as cnt"
." FROM eb_tags_links LEFT JOIN eb_tags"
." ON eb_tags.tag_id = eb_tags_links.tag_id LEFT JOIN eb_articles"
." ON eb_articles.article_id = eb_tags_links.article_id"
." WHERE article_flag = 1 OR article_flag = 2"
." GROUP BY eb_tags.tag_id ORDER BY cnt";

Archivo: _lib/news.php – Método: eb_tag($n)

$sql="SELECT eb_articles.article_id, article_title"
." FROM eb_articles LEFT JOIN eb_tags_links"
." ON eb_articles.article_id = eb_tags_links.article_id"
." LEFT JOIN eb_tags ON eb_tags.tag_id = eb_tags_links.tag_id"
." WHERE tag_name="" . $tag . "" AND article_flag = 1 OR article_flag = 2"
." ORDER BY article_title";

Como podéis ver no mentía, hay muchos cambios en _lib/news.php. Espero que el nuevo formato permita ver mejor las diferencias entre el código original y el modificado.

Sigamos con los cambios. Ahora le toca a _lib/admin.php. Aquí los cambios están concentrados en los métodos de crear y editar un artículo, por lo que mejor pongo los métodos enteros:

function eb_admin_editarticle( $id ) {
    global $lang;
    $output=" <h3>" . ucwords( $lang['admin'] ) . "</h3> ";
    if( !empty( $_POST['text'] ) ) {
        foreach( $_POST as $key => $value ) {
            $_POST[$key] = str_replace( "&Acirc;", "", $value );
        }
        if( strlen( $_POST['tags'] ) > 1 ) {
            mysql_query( "DELETE FROM eb_tags_links WHERE article_id=" . $id );
            $tags = explode( ",", $_POST['tags'] );
            foreach( $tags as $tag ) {
                $sql = "INSERT INTO eb_tags SET tag_name="" . $tag . """;
                mysql_query( $sql );
                if( mysql_affected_rows() == 1 ) {
                    $tag_ids[] = mysql_insert_id();
                }
                else{
                    $tag_ids[] = mysql_result(
                        mysql_query( "SELECT tag_id FROM eb_tags"
                                  ." WHERE tag_name="" . $tag . """), 0 );
                }
            }
            foreach( $tag_ids as $tag_id ) {
                mysql_query( "INSERT INTO eb_tags_links"
                          ." SET tag_id=" . $tag_id . ", article_id=" . $id );
            }
        }

        $creationDateIfDraft = "";

        $sql = "SELECT article_flag FROM eb_articles"
        ." WHERE article_id = " . $id;

        if( $row = mysql_fetch_array( mysql_query( $sql ) ) ) {
            $articleFlag = $row['article_flag'];
            if( $articleFlag == 3 ) {
                $creationDateIfDraft = "", article_date="" . time();
            }
        }

        $articleStatus = $_POST['sticky'];
        if( !empty( $_POST['saveAsDraft'] ) && $_POST['saveAsDraft'] == "yes" ) {
            $articleStatus = 3;
            $creationDateIfDraft = "", article_date="" . time();
        }

        $sql = "UPDATE eb_articles SET article_flag=" . $articleStatus
        .", article_title="" . str_replace( " ", "", $_POST['title'] )
        ."", article_body="" . $_POST['text']
        ."", article_comments=" . $_POST['comments']
        .", mod_date="" . time()
        .$creationDateIfDraft
        ."" WHERE article_id=" . $id;

        mysql_query( $sql );
        header( 'Location: news.php?id=' . $id );
    }
    else {
        $sql="SELECT article_title, article_body, article_comments, article_flag"
        ." FROM eb_articles WHERE article_id=" . $id;
        $query = mysql_query( $sql );
        $row = mysql_fetch_row( $query );

        $tags = "SELECT tag_name FROM eb_tags LEFT JOIN eb_tags_links"
        ." ON eb_tags_links.tag_id = eb_tags.tag_id WHERE article_id=" . $id;
        $tags = mysql_query( $tags );
        if( mysql_num_rows( $tags ) > 0 ) {
            while( $tags_row = mysql_fetch_array( $tags ) ) $tag_array[] = $tags_row[0];
            $tags = implode( ",", $tag_array );
        }
        else $tags="";

        $output .= '<script type="text/javascript"'
        .' src="_lib/scripts/ckeditor/ckeditor.js"></script>' . " ";

        $output .= " <form action="admin.php?editarticle="
        .$id . "" method="post"> ";
        $output .= " <p><b>" . ucwords( $lang['title'] )
        ."</b><br /><input class="inputtext" type="text" name="title""
        ." value="" . $row[0] . "" /></p> ";
        $output .= " <textarea id="text" name="text"></textarea> ";
        $data = str_replace( '"', '"', $row[1] );
        $data = str_replace( " ", "" +"", $data);
        $output .= '<script type="text/javascript">' . " "
        .'var editor = CKEDITOR.replace( "text" );' . " "
        .'editor.setData( "'.$data.'" )' . " "
        .'</script>';
        $output .= " <p><b>" . ucwords( $lang['tags'] ) . "</b><br />"
        .$lang['tag_seperate']."<br /><input type="text" class="inputtext""
        ." name="tags" value="" . $tags . "" /></p> ";

        // Sticky radio button
        $output .= " <p><b>" . ucwords( $lang['sticky'] )
        ."</b><br /><input type="radio" name="sticky" value="1" ";
        if( $row[3] == 1 || $row[3] == 3 ) {
            $output .= " checked="checked"";
        }
        $output .= "/> " . ucwords( $lang['no'] )
        ."<br /><input type="radio" name="sticky" value="2" ";
        if( $row[3] == 2 ) {
            $output .= " checked="checked"";
        }
        $output .= "/> " . ucwords( $lang['yes'] ) . "<br /></p> ";

        // Comments radio button
        $output .= " <p><b>" . ucwords( $lang['comments'] )
        ."</b><br /><input type="radio" name="comments" value="0" ";
        if( $row[2] == 0 ) $output .= " checked="checked"";
        $output .= "/> " . ucwords( $lang['no'] )
        ."<br /><input type="radio" name="comments" value="1" ";
        if( $row[2] == 1 ) $output .= " checked="checked"";
        $output .= "/> " . ucwords( $lang['yes'] ) . "<br /></p> ";
        $output .= " <input type="hidden" id="saveAsDraft""
        ." name="saveAsDraft" value="no" /> ";
        $output .= " <p><input type="submit" name="submit" value=""
        .ucwords( $lang['save'] ) . "" /> ";
        $output .= " <input type="submit" name="submitDraft" value=""
        .$lang['draft'] . "" onclick="setSaveAsDraftTrue();" /></p> ";
        $output .= " </form> ";
        $output .= '<script type="text/javascript">'
        .'function setSaveAsDraftTrue(){'
        .'var saveAsDraft = document.getElementById( "saveAsDraft" );'
        .'saveAsDraft.value = "yes";'
        .'}'
        .'</script>';


        eb_head('');
        echo $output;
        eb_foot();
    }
}

function eb_admin_newarticle() {
    global $lang;
    $output = " <h3>" . ucwords( $lang['new_article'] ) . "</h3> ";
    if( !empty( $_POST['text'] ) ) {
        foreach( $_POST as $key => $value ) {
            $_POST[$key] = str_replace( "&Acirc;", "", $value);
        }
        $articleStatus = $_POST['sticky'];
        if( !empty( $_POST['saveAsDraft'] ) && $_POST['saveAsDraft'] == "yes" ) {
            $articleStatus = 3;
        }
        $sql = "INSERT INTO eb_articles SET article_flag=" . $articleStatus
        .",article_date = "" . time() . "",mod_date="" . time()
        ."",author_id = "" . $_SESSION['user_id-' . $_SERVER['SERVER_NAME']]
        ."", article_title = "" . str_replace( """, " ", $_POST['title'] )
        ."", article_body = "" . $_POST['text']
        ."", article_comments = " . $_POST['comments'];

        mysql_query( $sql );

        $article_id = mysql_insert_id();

        if( strlen( $_POST['tags'] ) > 1 ) {
            $tags = explode( ",", $_POST['tags'] );
            foreach( $tags as $tag ) {
                $sql = "INSERT INTO eb_tags SET tag_name="" . $tag . """;
                mysql_query( $sql );
                if( mysql_affected_rows() == 1 ) {
                    $tag_ids[] = mysql_insert_id();
                }
                else{
                    $tag_ids[] = mysql_result(
                        mysql_query( "SELECT tag_id FROM eb_tags"
                                  . " WHERE tag_name="" . $tag . """), 0 );
                }
            }
            foreach( $tag_ids as $tag_id ) {
                mysql_query( "INSERT INTO eb_tags_links".
                           " SET tag_id=" . $tag_id . ",article_id=" . $article_id);
            }
        }

        header( 'Location: news.php?id=' . $article_id );
    }
    else {
        $output .= '<script type="text/javascript"'
        .' src="_lib/scripts/ckeditor/ckeditor.js"></script>' . " "
        ." <form action="admin.php?id=newarticle" method="post"> "
        ." <p><b>" . ucwords( $lang['title'] ) . "</b><br />"
        ."<input class="inputtext" type="text" name="title" /></p> "
        ." <textarea class="ckeditor" id="text" name="text">"
        ."</textarea> "
        ." <p><b>".ucwords( $lang['tags'] ) . " " . $lang['tag_seperate']
        ."<br /><input type="text" class="inputtext" name="tags" /></p> "
        ." <p><b>" . ucwords( $lang['sticky'] ) . "</b><br />"
        ."<input type="radio" name="sticky" value="1" checked="checked" /> "
        .ucwords( $lang['no'] ) . "<br />"
        ."<input type="radio" name="sticky" value="2" /> "
        .ucwords( $lang['yes'] ) . "</p> "
        ." <p><b>" . ucwords( $lang['comments'] )
        ."</b><br /><input type="radio" name="comments" value="0" /> "
        .ucwords( $lang['no'] ) . "<br />"
        ."<input type="radio" name="comments" value="1""
        ." checked="checked" /> " . ucwords( $lang['yes'] ) . "</p> "
        ." <input type="hidden" id="saveAsDraft" name="saveAsDraft""
        ." value="no" /> "
        ." <p><input type="submit" name="submit" value=""
        .ucwords( $lang['save'] ) . "" /> "
        ." <input type="submit" name="submitDraft" value=""
        .$lang['draft'] . "" onclick="setSaveAsDraftTrue();" /></p> "
        ." </form> "
        .'<script type="text/javascript">'
        .'function setSaveAsDraftTrue() {'
        .'var saveAsDraft = document.getElementById( "saveAsDraft" );'
        .'saveAsDraft.value = "yes";'
        .'}'
        .'</script>';

        return $output;
    }
}

El último cambio se realiza en sitemap.php, para evitar enviar a Google las URLs de los borradores:

Archivo: sitemap.php

$sql = "SELECT article_id, mod_date, article_title, article_date"
." FROM eb_articles"
." WHERE article_flag = 1 OR article_flag = 2"
." ORDER BY article_date DESC";

Me gustaría destacar la falta de indentación en el código original. Entiendo que en PHP es importante el rendimiento, pero el código spaghetti y los métodos de 200 líneas no facilitan la tarea de modificar el código. Si EggBlog fuese un producto acabado, entendería la optimización del código, pero no es así, está falto de muchas funcionaliades y tiene aspectos muy mejorables, por lo que considero que la legibilidad del código debería ser una prioridad.

También he sacado varias conclusiones con esta modificación. La primera es que compartir el código en los artículos no es la mejor opción, por lo que me estoy planteando subir el código a algún repositorio para poder compartirlo con todos.

La otra es que no me gusta el código actual de eggBlog. Es buen código en cuanto a que funciona muy bien y tiene muy pocos fallos (aunque he encontrado algunos que comentaré en un artículo posterior), pero el problema de la legibilidad hace que cada vez que toco algo, me entren ganas de reordenarlo todo.

Supongo que esto se debe a que principalmente he programado en Java, donde me gusta mantener el código extremadamente limpio, y que soy un gran fan del refactoring, donde la legibilidad es mucho más importante que el rendimiento.

Por ello he descubierto que aún se muy poco sobre las mejores técnicas de programación en PHP, por lo que he dedicado un tiempo en buscar artículos de buenas prácticas. Más adelante haré un artículo recopilatorio con las que me han parecido más interesantes.

Bueno, lo dejo ya con el que ha sido sin duda mi artículo más largo. Por suerte he podido usar la función de borradores para escribirlo! 😀

Añadir seguimiento de nuevos comentarios en eggBlog

[programación]

Otro de los pequeños fallos de EggBlog es su sistema de comentarios. El problema no es como se deja un nuevo comentario, que por lo que he podido probar funciona bastante bien. El problema es que no hay manera de saber si alguien ha dejado un nuevo comentario en un artículo antiguo.

Los artículos de portada no tienen tanto problema, ya que puede que te des cuenta al moverte por la página, pero si alguien deja un comentario en un artículo de hace unos meses, no hay un buen sistema para enterarse. O compruebas todos los artículos para ver si hay un comentario nuevo o lo miras en la base de datos, que es más rápido, pero tampoco es una buena solución.

Mi primera idea fue que cuando alguien deja un nuevo comentario, se enviara un correo al administrador que le avisara. Esta idea es bastante buena, ya que te mantiene informado rápidamente sobre comentarios nuevos. El problema es el posible aluvión de correos en un futuro, aunque siempre se puede poner un filtro o algo.

Me puse a investigar como enviar correos desde PHP y resulta ser bastante sencillo. En la página oficial de PHP está toda la información. Sin embargo, al realizar algunas pruebas descubrí que mi servidor actual, al ser gratuito, no soporta el envío de correos desde PHP, por lo que no me quedó otra opción que buscar una alternativa.

Empecé a pensar en otra solución. Si no podía realizar un aviso externo, tendría que añadir algún elemento visual a la página que me indicara que hay comentarios nuevos. Decidía añadir una nueva entrada en la barra lateral que indique el número de comentarios nuevos y te lleve a una página desde donde poder revisarlos.

Menu con el número de comentarios pendientes en EggBlogEl número a la derecha de “Moderar Comentarios” indica el número de comentarios nuevos desde la última vez que se limpió el listado.

Si pulsamos el enlace llegamos a una nueva página donde aparece un listado con todos los comentarios nuevos, y donde cada comentario es un enlace al artículo donde ha sido publicado. De esta manera tenemos una forma sencilla de revisarlos todos, y que no se quede alguno perdido entre artículos viejos. Por último tenemos un botón que limpia el listado y devuelve el contador a cero.

Las modificaciones al código para implementar esto son las siguientes:

Primero añadimos las traducciones a es.php:

$lang['author'] = "Autor/a";
$lang['clear_list'] = "Limpiar Listado";
$lang['comment'] = "Comentario";
$lang['date'] = "Fecha";
$lang['hiding_comments'] = "Ocultando comentarios anteriores a la fecha";

Lo siguiente es crear una función que devuelva una fecha formateada. Podríamos añadir esta función a _lib/global.php, pero yo he decidido crear un fichero PHP con funciones útiles llamado _lib/util.php. La función devuelve la fecha en el mismo formato que se ve en los artículos, y su código es:

function getNiceDate($date){
    global $lang;

    return date("g:ia",$date) . ", " . ucwords( $lang['day' . date( "w", $date )] ) ." "
    .date( "j", $date ) . " " . ucwords( $lang['month' . date( "n", $date )] )
    ." " . date( "Y", $date );    
}

Si creáis el fichero nuevo, habrá que incluirlo en _lib/global.php:

require_once '_lib/util.php';

A continuación ya podemos crear la página desde donde moderar los comentarios. La he llamado moderate-comments.php y su código es:

<?php

require_once '_lib/global.php';
global $config;

eb_pre();

if( empty( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) ) {
    header( "Location: index.php" );
}
elseif( !eb_checkadmin( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) ) {
    header( "Location: index.php" );
}
else {
    eb_head('');

    $commentsList = "";

    $currentTime = time();
    $showForm = true;

    if( !empty( $_POST['viewTime'] ) ) {
        $viewTime = $_POST['viewTime'];
        $showForm = false;

        $sql = "UPDATE eb_comments SET comment_date = " . $viewTime
        ." WHERE comment_id = 0";

        mysql_query( $sql );

        $commentsList = "<p>" . $lang['hiding_comments'] . ": "
        .getNiceDate( $viewTime ) . "</p>";
    }
    else{
        $sql = "SELECT u.user_name, c.comment_id, c.article_id,"
        ." c.comment_body, c.comment_date, a.article_title"
        ." FROM eb_comments c, eb_users u, eb_articles a"
        ." WHERE c.comment_date > ("
        ."    SELECT comment_date FROM eb_comments"
        ."    WHERE comment_id = 0 )"
        ." AND u.user_id = c.author_id"
        ." AND a.article_id = c.article_id"
        ." AND c.author_id <> " . $_SESSION["user_id-" . $_SERVER['SERVER_NAME']]
        ." ORDER BY c.comment_id";
        $query = mysql_query( $sql );

        while( $row = mysql_fetch_array( $query ) ) {
            $commentBody = $row[3];
            if( strlen( $commentBody ) > 70 ) {
                $commentBody = substr( $commentBody, 0, 70 ) . "...";
            }

            $commentsList .= "<p><b>" . $lang['author'] . ": " . $row[0] . "<br />"
            ."<b>" . $lang['date'] . ": " . getNiceDate( $row[4] ) . "<br />"
            ."<b>" . $lang['comment'] . ":</b> <a href=""
            .eb_links_article( $row[5], $row[2] )
            ."#comment" . $row[1] . "">" . $commentBody . "</a></p> ";
        }
    }
?>

<h3><?php echo ucwords( $lang['moderate_comments'] ) ?></h3>

<?php
    echo $commentsList;
    if( $showForm == true ) {
?>

<form name="resetCommentsTime" action="moderate-comments.php" method="post">
    <input type="hidden" name="viewTime" value="<?php echo $currentTime; ?>" />
    <input type="submit" name="submit" value="<?php echo $lang['clear_list']; ?>" />
</form>

<?php
    }
    eb_foot();
}
?>

Como se pude observar en el código, los enlaces no solo llevan al artículo, sí no también al propio comentario. Para ello debemos añadir el ancla correspondiente para cada artículo. Esto se realiza en el método eb_article() del fichero _lib/news.php, donde:

<p class="comment">

Pasa a ser:

<p class="comment"><a name="comment" . $comment[3] . ""></a>"

Ahora necesitamos añadir en la barra lateral el enlace a la página y mostrar el número de comentarios nuevos. Para ello, en _lib/global.php, en el método eb_foot(), sustituimos el if que comprueba si el usuario es el administrador, por esto:

if( eb_checkadmin( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) ) {
    echo " <li><a href="admin.php">" . ucwords( $lang['admin'] ) . "</a></li> ";

    $sql = "SELECT u.user_name, c.comment_id, c.article_id,"
    ." c.comment_body, a.article_title, c.comment_date"
    ." FROM eb_comments c, eb_users u, eb_articles a"
    ." WHERE c.comment_date > ( SELECT comment_date FROM eb_comments"
    ." WHERE comment_id = 0 ) AND u.user_id = c.author_id"
    ." AND a.article_id = c.article_id"
    ." AND c.author_id <> " . $_SESSION["user_id-" . $_SERVER['SERVER_NAME']];
    $numOfComments = mysql_num_rows( mysql_query( $sql ) );

    echo " <li><a href="moderate-comments.php">"
    .ucwords( $lang['moderate_comments'] ) . " (" . $numOfComments . ")</a></li> ";
}

Por último necesitamos introducir en la base de datos un comentario de referencia. Este comentario no será visto, ni pertenece a ningún artículo. Simplemente utilizaremos su campo de fecha como el momento a partir del cual un comentario se considera nuevo, y será este comentario el que se actualizará al pulsar el botón en la página de moderar comentarios. Podemos añadir el comentario con la siguiente sentencia SQL:

INSERT INTO eb_comments ( comment_id, author_id, article_id, comment_ip, comment_date, comment_body )
VALUES ( 0, 0, 0, 0, 0, 'comentario de referencia' );

Los únicos valores importantes son que el comment_id, article_id y comment_date sean cero.

Y con esto ya tenemos en marcha el sistema de seguimiento de nuevos comentarios. Para cualquier duda podéis usar los comentarios, que ahora estoy seguro de no dejarme ninguno sin leer 😀

Sección de enlaces

[programación]

Algo que echaba en falta en el blog era una sección de enlaces, donde poder mostrar enlaces a páginas que me parecen interesantes y que sigo con asiduidad. Al final me decidí por mostrar estos enlaces en la barra lateral. Esto habría sido muy sencillo realizarlo de manera estática añadiendo los enlaces por código, pero la idea era poder modificarlo vía web. Estos son los pasos que he realizado:

1. Añadir una entrada en el fichero de idioma (es.php):

$lang['links'] = "enlaces";

2. Crear una tabla en la base de datos donde se almacenarán los enlaces, y ya de paso, añadir la creación de la tabla a la instalación para una futura migración. Añadimos lo siguiente al método eb_install() en _lib/admin.php:

mysql_query("DROP TABLE links;");
$sql.="CREATE TABLE links (".
    "link_id int NOT NULL AUTO_INCREMENT,".
    "link_name varchar(255) NOT NULL,".
    "link_url varchar(255) NOT NULL,".
    "PRIMARY KEY (link_id)".
    ");";

3. Creamos la página desde donde se podrán editar los enlaces. Como quería algo sencillo, la edición de los enlaces se realiza en un textarea. Cada enlace está formado por dos líneas. La primera es el nombre del enlace, mientras que la segunda es la dirección del enlace. La página de edición es links.php y su contenido es el siguiente:

<?php

require_once '_lib/global.php';
global $config;
eb_pre();

if(empty($_SESSION['user_id-'.$_SERVER['SERVER_NAME']])){
    header("Location: index.php");
}
elseif( !eb_checkadmin( $_SESSION['user_id-' . $_SERVER['SERVER_NAME']] ) ) {
    header("Location: index.php");
}
else {
    eb_head('');

    $textAreaText = "";

    if(!empty($_POST['editLinks'])){
        $textAreaText = $_POST['linksText'];
        if(!empty($textAreaText)){
            $links = explode(" ", $textAreaText);
        }

        $sqlQuery = "DELETE FROM links";
        mysql_query($sqlQuery);

        $count = 0;
        for($i = 0; $i < count($links); $i+=2){
            $sqlQuery = "INSERT INTO links SET link_name = '".$links[$i]."'"
            .", link_url = '".$links[$i+1]."'";
            mysql_query($sqlQuery);
            $count++;
        }
        echo "<p>Se han guardado ".$count." enlaces.</p>";
        $textAreaText = str_replace(" ", "", $textAreaText);
    }
    else{
        $query = mysql_query("SELECT link_name, link_url FROM links");

        while( $row = mysql_fetch_array( $query ) ) {
            if(!empty($textAreaText)){
                $textAreaText.=' ';
            }
            $textAreaText.=$row['link_name'].' '.$row['link_url'];
        }

        $textAreaText = str_replace(" ", "", $textAreaText);
    }

    $saveText = ucwords($lang['save']);
?>

<h3><?php echo ucwords($lang['links'])?></h3>
<form name="linksForm" action="links.php" method="post">
    <textarea name="linksText" id="linksText" rows="10"></textarea>
    <p>
        <input type="submit" name="submit" value="<?php echo $saveText; ?>" />
        <input type="hidden" name="editLinks" value="TRUE" />
    </p>
</form>

<script type="text/javascript">
<!--
var textArea = document.getElementById( "linksText" );
textArea.value = "<?php echo $textAreaText ?>";
//-->
</script>

<?php
    eb_foot();
}
?>

4. Añadir un enlace en la página de administración para acceder a la página de edición de enlaces. Yo lo he puesto tras el enlace a los miembros. En la función eb_admin() añadimos:

echo " <li><a href="links.php">" . ucwords( $lang['links'] ) . "</a></li>";

5. Ahora solo nos falta mostrar los enlaces en la columna lateral. Para ello debemos modificar el método eb_foot() en _lib/global.php, añadiendo lo siguiente donde queramos que se muestren. Yo me he decidido por ponerlos entre las etiquetas y el listado de artículos:

echo showLinks();

La definición del método es esta:

function showLinks(){
    global $lang;

    $count = mysql_num_rows(mysql_query("SELECT link_id FROM links"));

    if($count == 0){
        return "";
    }    

    $output = '<div class="box"><strong>' . ucwords( $lang['links'] ) . '</strong><ul>';

    $query = mysql_query("SELECT link_name, link_url FROM links");

    while( $row = mysql_fetch_array( $query ) ) {
        $output.=" ".'<li><a href="'.$row['link_url'].'" target="_blank">'
        .$row['link_name'].'</a></li>';
    }

    $output.='</ul></div>';

    return $output;
}

Y con esto ya tenemos nuestra sección de enlaces. Ahora solo falta llenarla de enlaces interesantes! 😀

Adaptar el editor para mostrar código HTML

[programación]

En muchos de mis artículos muestro código HTML. Esto no supone ningún problema para el editor a la hora de crear un nuevo artículo, pero cuando intento editar el contenido de un artículo anterior surgen complicaciones.

Esto ocurre por la manera en la que se está asignando el contenido HTML que se recupera de la base de datos. El contenido se está asignando al textarea de la siguiente manera:

$output.=" <textarea id="text" name="text">".$row[1]."</textarea>";

El problema de hacerlo así, es que si hay etiquetas HTML en el contenido, se están interpretando como parte del código de la página, por lo que empiezan a aparecer cajas de texto y otros elementos en medio del propio artículo.

La solución es por tanto asignar el contenido de forma dinámica con javascript, y hacer esto con CKEditor es sencillo:

$output.=" <textarea id="text" name="text"></textarea>";
$data = str_replace('"','"',$row[1]);
$data = str_replace(" ", "", $data);
$output.='<script type="text/javascript">'
.'var editor = CKEDITOR.replace( "text" );'
.'editor.setData("'.$data.'")'
.'</script>';

Con esto lo que estamos haciendo es, primero crear un textarea vacío. Después, en el contenido del artículo escapamos todas las comillas dobles, y añadimos una suma en los cambios de línea para que no salte un error de javascript. Por último definimos el CKEditor y le asignamos el contenido.

Sustituir el editor de artículos de eggBlog

[programación]

Tras casi dos meses con el blog, uno de los puntos más flojos que le veo es el editor de artículos. EggBlog trae como editor por defecto openwysiwyg. Este editor tiene unos cuantos fallos gordos que empezaban a ser insufribles. Sólo por listar algunos, metía código innecesario en algunas de las etiquetas, todo el estilismo se realiza mediante spans y como factor decisivo, al intentar editar el código HTML no muestra ni un cambio de línea, te lo muestra todo en un único bloque. Vamos, imposible hacer cambios directamente en el código en esas condiciones.

Mirando un poco el código, veo que la fecha de última modificación del editor es de 2007, pero vamos, lo veo normal, seguro que si me voy a su web hay una versión nueva para descargar….. ¡Pues no! La versión más actual es de 2007. Cuatro años abandonado… ummm, hay que cambiarlo 😛

Buscando editores WYSIWYG en javascript, veo que hay dos alternativas predominantes, TinyMCE y CKEditor. Los dos generan código XHTML, son opensource y tienen buenas críticas. ¿Por cual decidirse entonces? Mirando como integrarlos, ambos son sencillos de usar y tienen buen código por si algún día me meto a modificarlos.

Pruebo la demo de los dos, y en cuanto a funcionalidad están muy a la par. Sin embargo, el CKEditor me resulta más agradable de usar… pero ese tampoco es un factor determinante. Lo mejor será preguntar a internet 😛

Buscando en Google “TinyMCE vs CKEditor” me encuentro con este artículo que los compara.

Artículo bastante interesante, que acaba diciendo lo que yo ya había supuesto, que ambos están muy a la par, con el CKEditor un poco por encima. Sigo buscando más comparativas y el CKEditor sale victorioso de la mayoría. Mis dudas se disuelven por completo cuando veo comentarios en los que se dice que el CKEditor es más estable, y el TinyMCE suele tener pequeños bugs.

¡CKEditor es el vencedor!

Ahora hay que integrarlo. ¿Será complicado? Para nada, en 5 minutos solucionado.

Estas 3 líneas definían el viejo editor:

$output .= " <script type="text/javascript" src="_lib/openwysiwyg/scripts/wysiwyg.js"> </script>";
$output.=" <script type="text/javascript" src="_lib/openwysiwyg/scripts/wysiwyg-settings.js"></script>";
$output.=" <script type="text/javascript"> WYSIWYG.attach( 'text', small ); </script>";

Las borramos y las sustituimos por lo siguiente:

$output.='<script type="text/javascript"'
.' src="_lib/scripts/ckeditor/ckeditor.js"></script>';

Ahora solo falta indicarle al editor sobre que textarea debe trabajar. Esto es tan simple como asignar una clase al textarea, por lo que esto:

$output.=" <textarea id="text" name="text">".$row[1]."</textarea>";

Pasa a ser esto:

$output.=" <textarea class="ckeditor" id="text" name="text">"
.$row[1]."</textarea>";

Hay que hacer estos cambios en los métodos eb_admin_editarticle y eb_admin_newarticle. Y con esto ya tenemos nuevo editor. ¡A disfrutarlo!, que la verdad es que es una gozada comparado con el anterior 😀

Acordeón multinivel para el listado de artículos

[programación]

Sigo cambiando cosas que no me gustaban de EggBlog.

El listado de artículos en la columna de la derecha me parecía un poco pobre. Solo mostraba los 10 últimos, por lo que los más antiguos iban quedando relegados al olvido. Se podía acceder a una página con el listado completo de artículos, pero me parecía muy feucha.

La idea era por tanto poder acceder al listado completo de artículos desde cualquier página. Quitar la limitación de 10 artículos es sencillo, pero claro, cuando tenga 100 el listado sería enorme. Hace tiempo trabaje en el curro con un acordeón en Prototype, y me gustaba su funcionalidad, por lo que pensé que sería buena idea tener un acordeón multinivel para los artículos, de forma que se pueda desplegar por años y por meses.

Aquél que usé recuerdo que era un poco “pesado” en cuanto a rendimiento, por lo que decidí usar JQuery en esta ocasión. Buscando un poco encontré este acordeón.

Tenía buena pinta, así que lo estuve probando, y me daba problema al intentar hacerlo multinivel (no realizaba correctamente las animaciones).

Buscando el enlace he encontrado este otro.

También tiene buena pinta, pero no dice nada de multinivel. Tendré que hacer la prueba un día a ver como se comporta.

El caso es que seguí buscando y al final encontré una implementación de un acordeón multinivel que no utiliza ninguna librería por debajo. La implementación es muy simple, el js es diminuto (1,35KB) y funciona a las mil maravillas.

Para implementar el acordeón, el método que hay que modificar es eb_articlelist en el fichero news.php. En principio pensé en adaptar el código existente, pero no me gustaba nada su implementación. Básicamente se hace una consulta SQL para cada mes. Esto puede tener sentido si como máximo se van a mostrar 10 artículos, pero si se quieren mostrar todos no lo veo muy eficiente. Por eso acabé modificando el código por completo:

/**
 * Compone un acordeón con todos los artículos publicados, agrupados por meses
 * y por años.
 * @param integer $n Si su valor es cero no se muestra el acordeón.
 */
function eb_articlelist($n) {
    global $lang;
    settype( $n, "integer" );
    if( $n == 0 ){
        return "";
    }
    $output = '<div class="box"><strong>' . ucwords( $lang['articles'] ) . '</strong>';
    $output .= " " . '<ul class="acc acc_sup" id="acc_sup">';
    $lastYearShown = 0;
    $lastMonthShown = 0;
    $query = mysql_query( "SELECT article_id, article_title, article_date"
    ." FROM eb_articles WHERE article_flag != 0"
    ." ORDER BY article_date DESC" );
    $countNestedAcc = 0;
    while( $row = mysql_fetch_array( $query ) ) {
        $articleDate = $row['article_date'];
        $currentArticleYear = date( "Y", $articleDate );
        $currentArticleMonth  = date( "n", $articleDate );
        if( $currentArticleYear != $lastYearShown ) {
            if( $lastYearShown != 0 ) {
                // Cerramos el año anterior
                $output .= '</ul></div></div></li>';
            }
            // Abrimos el nuevo año
            $output .= " " . '<li><h3>' . $currentArticleYear . '</h3>'
            ." " . '<div class="acc-section"><div class="acc-content">'
            ." " . '<ul class="acc nested" id="nested_'.$countNestedAcc.'">';
            $countNestedAcc++;
            $lastYearShown = $currentArticleYear;
        }
        if( $currentArticleMonth != $lastMonthShown ) {
            if( $lastMonthShown != 0 ) {
                // Cerramos el mes anterior
                $output .= " " . '</ul></div></div></li>';
            }

            // Abrimos el nuevo mes
            $output .= " " . '<li><h3>' . ucwords( $lang['month' . $currentArticleMonth] )
            . " " . $currentArticleYear . '</h3>'
            . " " . '<div class="acc-section"><div class="acc-content"><ul>';
            $lastMonthShown = $currentArticleMonth;
        }

        $output .= " " . '<li><a href="'
        .eb_links_article( $row['article_title'], $row['article_id'] ) . '">'
        .$row[1] . '</a></li>';
    }

    // Cerramos el último mes, año y acordeon
    $output .= " " . '</ul></div></div></li>'
    ." " . '</ul></div></div></li>'
    ." " . '</ul>';

    // Inicializamos los acordeones con javascript
    $output .= " ".'<script type="text/javascript" src="_lib/scripts/script.js"></script>'
    ." " . '<script type="text/javascript">';

    // Acordeón superior
    $output.=" ".'var parentAccordion = new TINY.accordion.slider( "parentAccordion" );'
    ." " . 'parentAccordion.init( "acc_sup", "h3", 0, 0);';

    // Acordeones anidados
    for( $i = 0; $i < $countNestedAcc; $i++ ) {
        if( $i == 0 ) {
            $ex = 0;
        }
        else{
            $ex = -1;
        }
        $output .= " " . 'var nestedAcc' . $i . '=new TINY.accordion.slider( "nestedAcc' . $i . '" );'
        ." " . 'nestedAcc' . $i . ' . init( "nested_' . $i . '", "h3", 1,' . $ex . ', "acc-selected" );';
    }

    // Fin del script
    $output .= " " . '</script>';

    // Fin del div box
    $output .= '</div>';

    return $output;
}

Colocamos el script del acordeón (script.js) en la carpeta _lib/scripts, y añadimos las imágenes a themes/default/images.

Por último hay que modificar los estilos para el acordeón, por lo que añadimos los siguientes estilos a style.css:

/* ESTILOS PARA EL ACORDEÓN */
.acc_sup {
    width:205px;
    list-style:none;
    color:#033;
    margin:0 auto 40px -16px;
}

.acc_sup h3 {
    width:165px;
    border:1px solid #9ac1c9;
    padding:6px 6px 8px;
    font-weight:bold;
    margin-top:5px;
    cursor:pointer;
    background:url(images/header.gif);
    font-size: 9pt;
    margin: 0;
}

.acc_sup h3:hover { background:url( images/header_over.gif ) }
.acc_sup .acc-section {
    overflow:hidden;
    background:#fff;
    width: 180px;
}

.acc_sup .acc-content {
    width:162px;
    padding:15px;
    border:1px solid #9ac1c9;
    border-top:none;
    background:#fff;
    padding-left: 0px;
}

.nested {
    width:180px;
    list-style:none;
    color:#033;
    margin-bottom:15px;
    margin-left: -7px;
}

.nested h3 {
    width:145px;
    border:1px solid #9ac1c9;
    padding:6px 6px 8px;
    font-weight:bold;
    margin-top:5px;
    cursor:pointer;
    background:url(images/header.gif);
}

.nested h3:hover { background:url( images/header_over.gif ) }

.nested .acc-section {
    overflow:hidden;
    background:#fff;
    width: 160px;
}

.nested .acc-content {
    width:127px;
    padding:15px;
    border:1px solid #9ac1c9;
    border-top:none;
    background:#fff;
}

.nested .acc-selected { background:url( images/header_over.gif ) }

Con esto me cargo la página con el listado de artículos. Total, ya no aporta ninguna funcionalidad, y para mostrar el mismo acordeón que en el resto de las páginas, ya no tenía mucho sentido 😀

Añadiendo paginación al blog

[programación]

Pese a lo que me está gustando eggBlog, tiene algunas decisiones que me parecen incomprensibles. La principal es que solo se muestren 3 artículos en la página principal y no exista ningún tipo de paginación.

Así que me puse a realizar las modificaciones necesarias para añadir paginación y que el número de artículos a mostrar en las páginas fuese configurable.

Lo primero es añadir el parámetro de configuración en config.php. En mi caso, por defecto quiero mostrar 5 artículos por página:

$config['posts_per_page'] = "5";

Para hacerlo configurable debemos modificar admin.php. El método eb_admin_config se encarga de mostrar los parámetros de configuración. En el “if” se muestran por pantalla los parámetros almacenados, mientras que el “else” se encarga de construir el formulario para modificarlos. En el if introducimos:

$output.=" <tr><th>" . ucwords( 'Artículos por página' ) . "</th><td>"
.str_replace( "&Acirc;", "", htmlentities( $config['posts_per_page'], ENT_QUOTES ) )
."</td></tr>";

Mientras que en el else introducimos:

$output.=" <tr><th><label for="">" . ucwords( 'Artículos por página' )
."</label></th><td><input class="inputtext" id="" type="text""
."name="posts_per_page" value=""
.htmlentities( $config['posts_per_page'], ENT_QUOTES )."" /></td></tr>";

De la paginación en sí, se encarga una nueva página llamada page.php. Su contenido es el siguiente:

<?php
require_once "_lib/global.php";
if( !empty( $_GET['id'] ) ) {
    settype( $_GET['id'], "integer" );
}
eb_pre();
eb_head('');
$articles = getArticlesIdsForPage( $_GET['id'], $config['posts_per_page'] );
if( count( $articles ) > 0 ) {
    echo eb_article( 0, $articles );
}

showPagination( $_GET['id'], $config['posts_per_page'] );
eb_foot();
?>

Los nuevos métodos que se encargan de la paginación se encuentran en news.php y son los siguientes:

/**
 * Obtiene un vector con los IDs de los N artículos que se deben mostrar en la
 * página indicada.
 *
 * @param integer $page El número de la página a mostrar
 * @param integer $n El número de artículos a mostrar en la página
 */
function getArticlesIdsForPage( $page, $n ) {
    settype( $page, "integer" );
    settype( $n, "integer" );
    $articleIds = array();
    $sql = "SELECT article_id FROM eb_articles WHERE article_flag = 1"
          ."ORDER BY article_date DESC";
    $query = mysql_query( $sql );
    while( $row = mysql_fetch_array( $query ) ) {
        $articleIds[] = $row['article_id'];
    }

    $startIndex = ( $page - 1 ) * $n;
    $endIndex = $startIndex + $n - 1;

    if( $startIndex > ( count( $articleIds ) - 1 ) ) {
        return array();
    }

    if($endIndex > ( count( $articleIds ) - 1 ) ){
        $endIndex = count( $articleIds ) - 1;
    }

    $output = array();

    for( $i = $startIndex; $i <= $endIndex; $i++ ){
        $output[] = $articleIds[$i];
    }

    return $output;
}

/**
 * Se encarga de mostrar los enlaces a artículos anteriores y posteriores
 * dependiendo de la página en la que se encuentra el usuario.
 *
 * @param integer $currentPage La página actual
 * @param integer $articlesPerPage El número de artículos por página
 */
function showPagination( $currentPage, $articlesPerPage ){
    settype( $currentPage, "integer" );
    if( $currentPage > 1 ){
        showPrevPageLink( $currentPage );
    }

    if( count( getArticlesIdsForPage( $currentPage + 1, $articlesPerPage ) ) > 0 ){
        showNextPageLink( $currentPage );
    }
}

/**
 * Muestra el enlace a la página anterior.
 *
 * @param integer $currentPage La página actual
 */
function showPrevPageLink( $currentPage ){
    echo '<div class="paginacionPrev"><a href="page-' . ( $currentPage - 1 )
    .'.html"><- Artículos recientes</a></div>';
}

/**
 * Muestra el enlace a la página siguiente.
 *
 * @param integer $currentPage La página actual.
 */
function showNextPageLink( $currentPage ){
    echo '<div class="paginacionNext"><a href="page-' . ( $currentPage + 1 )
    .'.html">Artículos anteriores -></a></div>';
}

Por último modificamos el index.php para utilizar el nuevo parámetro y mostrar la paginación:

<?php
require_once '_lib/global.php';

global $config;
eb_pre();
eb_head('');

$articles = eb_articles( $config['posts_per_page'] );

if( count( $articles ) > 0 ) {
    echo eb_article( 0, $articles );
}

showPagination( 1, $config['posts_per_page'] );

eb_foot();
?>

Para que la paginación también utilice URL semánticas, debemos modificar el .htaccess, añadiendo esta como la primera regla:

RewriteRule page-([0-9]+).html$ page.php?id=$1 [L]

Crear un sitemap.xml de forma dinámica con PHP

[programación]

Siguiendo con las optimizaciones para mejorar la indexación por parte de Google, vi en las herramientas que proporciona Google a webmasters que se puede proporcionar un sitemap en formato xml para que Google pueda rastrear con mayor facilidad algunas páginas de difícil rastreo.

En teoría, con la actualización de las Friendly URLs ya no tengo páginas de difícil rastreo, pero viendo que no me indexa, pues mejor dar todas las facilidades posibles.

Lo primero que hice fue crear un sitemap.xml mediante esta página.

La página se encarga de rastrear el sitio y obtener el listado de enlaces y te genera un xml para descargar. Después se deja en la raíz del blog, y ya es accesible en la ruta /sitemap.xml.

El problema es que según se generan nuevos artículos, tendría que estar volviendo a generar manualmente el sitemap. Además, aparecían enlaces que no estoy especialmente interesado en que se indexen, como las etiquetas o la página de registro.

La solución, por tanto, es que el sitemap se genere de forma dinámica. Estuve buscando generadores de sitemap en PHP y al final encontré este tutorial de como hacer el tuyo propio. Lamentablemente el tutorial ya no está disponible.

Usando ese código como base y el sitemap.xml que había generado con la primera página como plantilla, lo he adaptado para poder funcionar con la base de datos de eggBlog.

Al final, solo añado las URLs de los artículos, ya que son mi prioridad en la indexación, pero no debería ser complicado modificar el código para añadir más enlaces.

[ACTUALIZACIóN 05/03/2011]

He realizado algunas modificaciones al sitemap.php. Anteriormente estaba utilizando la fecha de publicación como la fecha de modificación el el sitemap, pero claro, yo quiero indicar a Google que un articulo ha sido actualizado, por lo que necesito tener una fecha de modificación. Para eso añadí una nueva columna a la tabla “eb_articles” con el nombre “mod_date”.

Hay que establecer su valor en dos métodos, eb_admin_editarticle y eb_admin_newarticle, ambos en admin.php. Simplemente hay que añadir .",mod_date="".time(). en el INSERT.

También es interesante añadirlo a la definición de la tabla en el método eb_install para una futura migración o reinstalación. Solo hay que añadir:

"mod_date int NOT NULL DEFAULT '0',".

Por si alguien lo quiere aprovechar, aquí os dejo el código para descargar. Me ha tocado subirlo como un txt, ya que no me deja subir ficheros zip… ¬

[ACTUALIZACIóN 11/10/2011]

Con tanto cambio de servidor, el fichero zip se perdió en algún lado, así que a continuación pongo la útima versión que hice para eggBlog. Para wordpress lo mejor es optar por un plugin como el Google (XML) Sitemap Generator.

<?php
require_once "_lib/global.php";
header( "Content-Type: text/xml;charset=iso-8859-1" );

// Connectamos con la base de datos
mysql_connect( $config['mysql_host'], $config['mysql_user'], $config['mysql_pass'] );
@mysql_select_db( $config['mysql_db'] ) or die( "Unable to select DB" );

// Realizamos la consulta
$sql = "SELECT article_id, mod_date, article_title, article_date"
." FROM eb_articles"
." WHERE article_flag = 1 OR article_flag = 2"
." ORDER BY article_date DESC";
$query = mysql_query( $sql ) or die( "Query failed" );

// Escribimos la cabezera del xml
echo '<?xml version="1.0" encoding="UTF-8"?>
    <urlset
      xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
            http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">';

$maxArticleDate = 0;
settype( $maxArticleDate, "integer" );

while( $row = mysql_fetch_array( $query ) ) {

    // Obtenemos la Friendly URL del articulo
    $article_link = eb_links_article( $row['article_title'], $row['article_id'] );

    // Componemos la fecha de modificación (p.e. 2011-02-18)
    $year = date( "Y", $row[1] );
    $mon  = date( "m", $row[1] );
    $day  = date( "d", $row[1] );

    $lastModDate = $year . '-' . $mon . '-' . $day;

    echo
        '<url>
            <loc>' . $article_link . '</loc>
            <lastmod>' . $lastModDate . '</lastmod>
            <changefreq>monthly</changefreq>
        </url>';

    $articleDate = $row['article_date'];
    settype( $articleDate, "integer" );
    if( $articleDate > $maxArticleDate ) {
        $maxArticleDate = $articleDate;
    }
}

mysql_close(); //Cerramos la conexión

$year = date( "Y", $maxArticleDate );
$mon  = date( "m", $maxArticleDate );
$day  = date( "d", $maxArticleDate );

$lastCreateDate = $year . '-' . $mon . '-' . $day;

echo
    '<url>
        <loc>' . $config['domain'] . 'index.php</loc>
        <lastmod>' . $lastCreateDate . '</lastmod>
        <changefreq>daily</changefreq>
    </url>';

echo '</urlset>';
?>

¿Indexa Google las páginas PHP?

[programación]

En teoría sí, pero por alguna razón a mi no.

Encontré este artículo que trata el tema de la indexación de páginas PHP por parte de Google y esclarece muchos de los mitos.

Ahí se explica que no debería haber ningún problema con las páginas generadas dinámicamente, pero en el caso de eggBlog, no parece indexar los artículos. En este caso se accede a los artículos a través de la página news.php y una ID, por ejemplo:

http://tu-dominio.com/news.php?id=12

Parece que el consenso general es que Google tiene problemas, o se esfuerza menos, en indexar páginas que tienes más de 2 parámetros, pero en mi caso solo tengo 1, el id, y no debería dar problemas…

El caso es que en el foro de eggBlog encontré un hilo en el que comentaban como sustituir la utilización de IDs por Friendly URLs. El usuario Faris se curró una modificación para el blog para implementar las Friendly URLs

Tras probar la modificación, estaba casi a punto, pero daba un fallo al intentar ir a los enlaces. Parecía cosa del .htaccess, así que tras investigar un poco encontré la respuesta. Solo hacía falta añadir esta linea:

RewriteBase /

El código completo del .htaccess por si alguien lo necesita es el siguiente:

Options +FollowSymLinks
RewriteEngine on
RewriteBase /
RewriteRule .*-([0-9]+).htm$ news.php?id=$1 [L]
RewriteRule .*-([0-9]+)/$ news.php?id=$1
RewriteRule (.*)-([0-9]+)$ $1-$2/ [R]
RewriteRule .*/([0-9]+)/$ news.php?id=$1
RewriteRule (.*)/([0-9]+)$ $1/$2/ [R]

Y aquí la página que suelo usar para trabajar experimentar con expresiones regulares.

El resultado final es que los enlaces a la derecha que van a los articulos muestran en la URL el nombre del artículo y el rastreador de Google debería de entrar en todos. A ver que tal en la próxima indexación…

[ACTUALIZACIÓN 15/02/2011]

Es irónico que la entrada que habla sobre Friendly URLs tenga una que justamente falla. ¿El problema? Pues que se está utilizando el título de la entrada como URL y ahí tengo un bonito interrogante (?), que se usa para pasar parámetros… y peta. Al principio pensaba que también estaban afectando el otro interrogante y el acento, pero solo quitando el interrogante de cierre se ha solucionado, aunque la URL ya no quedaba “bonita” ya que los caracteres especiales se sustituían por su código, por lo que he acabado sustituyendolos todos.

El método eb\_links\_article del fichero _lib/global.php queda así:

// F4ris - 20090708 - Function to produce SEO friendly links, dependant on config flag.
function eb_links_article( $title,$id ) {
    global $config;
    if( $config['seo_flag'] == 1 ) {
        $unsuportedChars = array( "&aacute;", "&eacute;", "&iacute;", "&oacute;", "&uacute;", "&iquest;", "?" );
        $suportedChars = array("a", "e", "i", "o", "u", "", "");
        $tmpTitle = str_replace( $unsuportedChars, $suportedChars, $title );
        $link = $config['domain'] . urlencode( str_replace(" ", "-", str_replace( " - ", "-", $tmpTitle ) ) ) . "-" . $id . ".htm";
    } else {
        $link = $config['domain'] . "news.php?id=" . $id;
    }
    return $link;
}