Contando el número de “likes” en un blog WordPress
Desde que incluimos un botón “me gusta” en todas las páginas de Los Fogg, creamos una página de Los Fogg en Facebook, y enlazamos la cuenta de Twitter con el muro de Los Fogg, Facebook es nuestra fuente de tráfico número uno. No hay duda que tanto Facebook como Twitter se han convertido en el nuevo RSS, una tecnología que nunca llegó a conseguir seguidores entre los no-geeks (los infieles).
Hemos subido más de 600 fotos al blog desde agosto de 2010 y tenía curiosidad por saber cuales eran las que tenían más “me gusta” en Facebook. El método cro-magnon es mirarse todas las páginas y apuntar el número que sale en el botón “me gusta”. O refinando un poco, ponerlas en un Excel para luego ordenarlas automáticamente.
Oigo una voz, mi geek interno, el maestro Yoda, que me dice: “Joven Padawan, esto es escriptable” (si el verbo escriptar no está en la DRAE, que alguien les escriba un email). Vamos a ver lo que dice el SDK de Facebook:
“El SDK de Javascript pone a tu disposición un extenso conjunto de funcionalidades para acceder desde el cliente al API de los servidores de Facebook. Estas incluyen toda la funcionalidad del REST API, Graph API y Dialogs. También dota a los desarrolladores de un mecanismo para renderizar versiones XFBML de nuestros Plugins Sociales.
Tienes que tener un app id para inicializar el SDK, que se puede obtener de la Developer App.”
Vale. No entiendo nada. Esto de pasarme 4 años sin programar no es bueno, ya me lo decía mi abuela. A ver por donde le hinco a esto el diente:
- Me tengo que sacar el “app id”. Fácil.
- Tengo que investigar que es el XFBML
- Tengo que investigar que es el Graph API
XFBML
Según he entendido en el parrafazo anterior, es una manera de renderizar los Plugins Sociales (botón “me gusta”, etc.). Tirando un poco del hilo, veo que es una extension de XHTML con tags propios para añadir versiones precocinadas de los Plugins Sociales a una página web (la otra opción es usar iframes). No voy a seguir leyendo esta tarjetita.
Graph API
Esto ya es más interesante. En la documentación, el Graph API está en la sección de conceptos fundamentales (core concepts):
“El Graph API presenta una vista sencilla y consistente del grafo social de Facebook, representando de manera unforme los objetos en el grafo (gente, fotos, eventos, páginas, etc.) y las conexiones entre ellos (relaciones entre amigos, contenido compartido, etiquetas de fotos, etc.).
Cada objeto en el grafo tiene un identificador único y se puede acceder en https://graph.facebook.com/identificador.”
Es decir, ¿que puedo acceder al nodo de mi página Facebook de Los Fogg en https://graph.facebook.com/losfogg
{
"id": "161137637259282",
"name": "Los Fogg",
"picture": "http://profile.ak.fbcdn.net/hprofile-ak-ash2/50254_161137637259282_2241030_s.jpg",
"link": "http://www.facebook.com/losfogg",
"likes": 185,
"category": "Personal blog",
"website": "http://losfogg.com",
"username": "losfogg",
}
Esto ha sido fácil. ¿Y si intento ver la cuenta de “likes” de una página en concreto? Vamos a probar con la URL https://graph.facebook.com/http://losfogg.com/2011/08/09/banya-la-sauna-rusa/
{
"id": "http://losfogg.com/2011/08/09/banya-la-sauna-rusa/",
"shares": 5
}
Bingo.
Listando las entradas en WordPress
Esto parece fácil: creo una página en WordPress que liste todas las URLs de las entradas (en mi caso, de la categoría “pics”) y luego le paso las URL al Javascript API y listo. Para hacer esto, me creo una plantilla de página de WordPress, y crear una nueva página que use esa plantilla (se puede cambiar en “atributos de la página”):
<span class="Apple-style-span" style="font-family: Consolas, Monaco, monospace; font-size: 12px; line-height: 18px; white-space: pre;">/* </span>
Template Name: Plantilla para contar likes de Facebook
*/
?>
<html>
<head><title>Facebook likes test</title></head>
<body>
<ol id="link_list">
<?php
global $post;
$args = array( 'posts_per_page' => -1, 'category_name' => 'pics' );
$myposts = get_posts( $args );
foreach( $myposts as $post ) :
setup_postdata($post); ?>
<li><?php the_permalink(); ?></li>
<?php endforeach; ?>
</ol>
</body>
</html>
Llamando al Javascript API de Facebook
Siguiendo el manual del API, lo primero es cargar el Javascript SDK en nuestra plantilla de WordPress:
<!-- Cargamos el Javascript API de Facebook -->
<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
FB.init({
appId : 'PON_AQUI_TU_ID',
status : true, // check login status
cookie : true, // enable cookies to allow the server to access the session
xfbml : true, // parse XFBML
oauth : true // enable OAuth 2.0
});
</script>
Y ahora solamente tenemos que consultar el grafo con el Javascript API. El método a usar es FB.api(), que como primer argumento toma la URL del grafo, y como segundo, una función callback… ojo… que estas son asíncronas, es decir que cuando el Javascript API haya terminado de consultar el valor a los sevidores de Facebook, entonces llamará a la función, no necesariamente siguiendo el órden en el que tenemos escritp el programita.
El pequeño lío aqui es que primero genera el HTML/javascript al ejecutar el PHP, y luego, dependiendo de las acciones en la página (onLoad, onClick, etc.), se ejecutará el Javascript. Para no liarme, uso el método “divide y vencerás”: primero vamos a llamar al Javascript API con un valor hardcoded (“a piñón fijo”) y luego vamos a ir añadiendo funcionalidades:
<script type="text/javascript">
function alertame() {
FB.api("/http://losfogg.com/2011/08/09/banya-la-sauna-rusa/", function(response) {
alert(response.shares);
});
}
</script>
La función alertame() la llamamos en el evento onload del elementoy efectivamente, nos sale una ventanita con el número de likes. Vamos bien.
Mezclando el PHP con el Javascript: ¿DOM o no DOM?
Idealmente, en el PHP, cuando generamos nuestro
- con el permalink, podríamos llamar al Javascript para que nos averiguara el número de likes, y así lo añadimos. Pero el problema es que el Javascript se ejecuta en el navegador, y el PHP en el servidor, así que tenemos que buscar otra manera.
Una opción sería usar el PHP API de Facebook que tan ricamente hemos ignorado desde el principio. Pero ya estamos tan cerca… ya casi saboreo la victoria (que inocente, si yo hubiera sabido…).
La siguiente opción que se me ocurre es, una vez generado el HTML, podemos recorrer el DOM, averiguar las URLs del grafo y hacer unas llamaditas al Javascript. Lo que pasa es que lo de manipular el DOM suele ser muy costoso computacionalmente y me da la impresión de que me voy a meter en camisa de once varas.
La opción que me parece más viable y fácil es hacer que el PHP me genere el Javascript directamente:
- Declaramos un Array que contenga todas las URLs
- Por cada URL, hacemos la llamada al API de Facebook y obtenemos el número de likes
- Añadimos un elemento a la lista manipulando el DOM lo justo
<script type="text/javascript">
function creaLista() {
// Crea un array con los enlaces a las paginas
var allLinks = new Array( <?php
global $post;
$args = array( 'posts_per_page' => -1, 'category_name' => 'pics' );
$myposts = get_posts( $args );
foreach( $myposts as $post ) :
setup_postdata($post); ?> "<?php the_permalink(); ?>",
<?php endforeach; ?> "popme");
allLinks.pop(); // Truco sucio: he puesto un elemento de mas,
// popme, para no tener que borrar la ultima coma
// Itera el Array
// aqui ponemos los elementos, un poquito de DOM
var linkListRef = document.getElementById("link_list");
for (var i=0;i<allLinks.length;i++) {
var oneLink = allLinks[i];
var fbGraphNode = "/"+oneLink;
// preceder con slash, para que se haga un elemento del grafo
// OJO, llamada asincrona
FB.api(fbGraphNode, function(response) {
// Crear el <li>
var anElement = document.createElement("li");
anElement.appendChild(document.createTextNode("("+response.shares+") "));
// elemento <a>
var linkElement = document.createElement("a");
// lo que va entre <a> y </a>
linkElement.appendChild(document.createTextNode(response.id));
linkElement.setAttribute("href",response.id); // el href
// ponemos el <a> dentro del <li>
anElement.appendChild(linkElement);
// ponemos el <li> dentro del <ol> (variable global)
linkListRef.appendChild(anElement);
});
}
}
</script>
En la parte de HTML pura, hay que darle un id al elemento en el que queremos crear la lista:
<ol id="link_list"></ol>
El script de arriba es bastante denso, pero he puesto comentarios que espero que os ayuden a clarificar un poco lo que voy haciendo.
Ordenando los resultados: problemas con los arrays asociativos en Javascript
Para rematar la faena, solamente tenemos que ordenar la lista por orden descendente de likes. Googleando un poco, veo que Javascript tiene una estructura de datos muy parecida a las tablas hash de PHP. Ponemos ahí los URLs como key y el número de likes como valor, ordenamos y ya ¿no?.
Para ahorraos el disgusto y la pérdida de tiempo, os cuento: los arrays asociativos de Javascript no se comportan como Arrays, sino simplemente como objetos a los que estamos añadiendo propiedades. Cuando añadimos myArray [key] = “value” estamos creando una propiedad “key” en el objeto myArray. El resultado es que al ponerse a iterar, me salían muchas más keys de las que había añadido y se liaba el tema al intentar llamar al API de Facebook.
Lo mejor es usar un Array de tuplas: myArray.push([key, value]), es decir, un array de arrays. En el script anterior, la llamada al API de Facebook quedaría:
FB.api(fbGraphNode, function(response) {
linkArray.push([response.id, response.shares]);
});
Ahora solamente nos queda ordenar ese Array, para lo cual podemos redefinir la función sort() del array, para que sepa como comparar nuestras tuplas con la URL y el número de likes.
linkArray.sort(function(a, b) { // Gracias http://bit.ly/pvzJIV
a = a[1];
b = b[1];
if(a == undefined) a = Number.MIN_VALUE;
if(b == undefined) b = Number.MIN_VALUE;
return a < b ? 1 : (a > b ? -1 : 0);
});
Solamente una pega: No podemos hacer la iteración sobre linkArray directamente después de la llamada a FB.api, porque las llamadas son asíncronas. Si lo hacemos, iteraríamos sobre un array vacío, ya que el API no ha devuelto ningún valor aún, cuando nuestro hilo de ejecución hace la iteración.
La solución es solamente hacerlo cuando linkArray esté lleno, al volver de la última llamada al API. El PHP final se nos queda así:
<body onload="creaLista();">
<!-- Cargamos el Javascript API de Facebook -->
<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
FB.init({
appId : '185062228233114',
status : true, // check login status
cookie : true, // enable cookies to allow the server to access the session
xfbml : true, // parse XFBML
oauth : true // enable OAuth 2.0
});
</script>
<script type="text/javascript">
<!--
function creaLista() {
// Crea un array con los enlaces a las paginas
var allLinks = new Array( <?php
global $post;
$args = array( 'posts_per_page' => -1, 'category_name' => 'pics' );
$myposts = get_posts( $args );
foreach( $myposts as $post ) : setup_postdata($post); ?> "<?php the_permalink(); ?>", <?php endforeach; ?> "popme");
allLinks.pop(); // Truco sucio: he puesto un elemento de mas, popme, para no tener que borrar la ultima coma
// Itera el Array
var linkListRef = document.getElementById("link_list"); // aqui ponemos los elementos, un poquito de DOM
var linkArray = []; // Aqui ponemos los elementos para ordenarlos. Es un array de tuplas.
for (var i=0;i<allLinks.length;i++) {
var oneLink = allLinks[i];
var fbGraphNode = "/"+oneLink; // preceder con slash, para que se haga un elemento del grafo de FB
// OJO, llamada asincrona
FB.api(fbGraphNode, function(response) {
linkArray.push([response.id, response.shares]);
if (linkArray.length == allLinks.length) { // El array esta lleno
linkArray.sort(function(a, b) { // Thanks http://bit.ly/pvzJIV
a = a[1];
b = b[1];
if(a == undefined) a = Number.MIN_VALUE;
if(b == undefined) b = Number.MIN_VALUE;
return a < b ? 1 : (a > b ? -1 : 0);
});
for (var j=0;j<linkArray.length;j++) {
var anElement = document.createElement("li");
anElement.appendChild(document.createTextNode("("+linkArray[j][1]+")"));
var linkElement = document.createElement("a");
linkElement.appendChild(document.createTextNode(linkArray[j][0]));
linkElement.setAttribute("href",linkArray[j][0]);
anElement.appendChild(linkElement);
linkListRef.appendChild(anElement);
}
}
});
}
}
-->
</script>
<ol id="link_list">
</ol>
</body></html>