Archive for October, 2007

Nuevo widget UWA de ipsojobs.com

Esta semana hemos estado preparando un widget UWA en ipsojobs, UWA significa “Universal Widget API” y es una especie de standard creado por netvibes para poder crear Widgets en cualquier plataforma (Netvibes, igoogle, opera, iPhone) una especie de “write once, run everywhere” que prometia Sun con Java en su momento pero aplicado al mundo de los Widgets.

Ya puestos a crear un widget, lo hemos creado con UWA, aunque es posible que si salen incompatibilidades debamos crear n-widgets para las n-plataformas sobre las que queramos desarrollar.

Ipsojobs UWA Widget in action

El widget pretende ser algo un poco más complejo que un simple lector de RSS, en concreto los objetivos del widget son:

  • Permitir seleccionar fácilmente la ciudad sobre la que queremos consultar las ofertas de trabajo de ipsojobs.
  • La selección de la ciudad debe ser “dinámica” dado que cada día aparecen ciudades nuevas no puede ser una lista estática.
  • Cuando estas viendo las ofertas de una ciudad siempre podrás cambiar a otra ciudad fácilmente.
  • El número de ofertas por ciudad debe ser personalizable.
  • Una vez seleccionada una ciudad el widget debe “recordar la preferencia” y mostrar las ofertas de la ciudad hasta que el usuario decida cambiarla.

Si quereis probarlo simplemente podéis visitar http://www.ipsojobs.com/widgets/uwa.html

El código del widget UWA es relativamente sencillo, se basa en un sólo fichero XHTML con todos los javascripts y los CSS incrustados.

Declaración:

<?xml version=”1.0″ encoding=”utf-8″?>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml” xmlns:widget=”http://www.netvibes.com/ns/” >
<head>

<meta name=”author” content=”Agusti Pons, omatech.com” />
<meta name=”description” content=”ipsojobs.com find jobs easy in your city” />

<meta name=”apiVersion” content=”1.0″ />
<meta name=”autoRefresh” content=”20″ />
<meta name=”debugMode” content=”true” />

<link rel=”stylesheet” type=”text/css” href=”http://www.netvibes.com/themes/uwa/style.css” />
<script type=”text/javascript” src=”http://www.netvibes.com/js/UWA/load.js.php?env=Standalone”>

De esta parte, sólo cabe destacar que se debe modificar el autor y la descripción así como el parámetro debugMode, que en producción deberá ser false. El debugMode=true es muy practico porque manda los mensajes de error a la consola del navegador y se puden consultar con el Firebug por ejemplo.

Javascripts útiles:


<script type=”text/javascript”>
<![CDATA[
function showhide(e)
{
//el = document.getElementById(e);
el = widget.body.getElementsByClassName(e)[0];
el.style.display = el.style.display == “block” ? “none” : “block”;
}

function getlang()
{
return widget.lang.substr(0, 2);
}

function link(url)
{
if (widget && widget.openURL)
{
widget.openURL(url);
}
}

function getSelected(ctrl)
{
for(i=0;i
{
if(ctrl[i].checked)
{
return ctrl[i].value;
}
}
return ”;
}

function setcity(url)
{
widget.setBody(’<p>…</p>’);
widget.setValue(’url’, url);
UWA.Data.getFeed(widget.getValue(’url’)+’?lang=’+getlang(), ipsowidget.display);
}

function selectcity()
{
widget.setTitle(’ipsojobs.com widget - setting…’);
UWA.Data.getText(’http://www.ipsojobs.com/api/controller.php?action=zones_sites_list&xsl=netvibes〈=’+getlang(), ipsowidget.zones_sites_processor);
}

function replace(texto,s1,s2)
{
return texto.split(s1).join(s2);
}

function get_change_city()
{
var lang = getlang();
if (lang==’es’)
{
return ‘Cambiar ciudad’;
}
if (lang==’ca’)
{
return ‘Canviar ciutat’;
}
if (lang==’fr’)
{
return ‘Changer de ville’;
}
else
{
return ‘Select city’;
}
}
]]>
</script>

Las dos funciones más interesantes son setcity() y selectcity() que detallo a continuación:


function setcity(url)
{
widget.setBody(’<p>…</p>’);
widget.setValue(’url’, url);
UWA.Data.getFeed(widget.getValue(’url’)+’?lang=’+getlang(), ipsowidget.display);
}

Recibe como parámetro la url del RSS que nos han clicado en la pagina de selección de la ciudad, primero ponemos unos puntos suspensivos para indicar que estamos cargando (widget.setBody(’<p>…</p>’);) posteriormente actualizamos el valor de la preferencia url (recordad que es oculta y el usuario no la puede ver) con la instrucción widget.setValue(’url’, url); y finalmente cargamos el feed RSS en el body, fijaos que utilizamos la url de la preferencia que acabamos de actualizar pero le añadimos el parámetro lang basándonos en el language del navegador del usuario UWA.Data.getFeed(widget.getValue(’url’)+’?lang=’+getlang(), ipsowidget.display);.

Para mostrar el RSS feed en el body fijaros que llamamos a la función display del widget que veremos más abajo declarada como ipsowidget.display = function(feed) esta función es muy parecida a la que se explica en el HOWTO de como mostrar un RSS en la web de Netvibes.


function selectcity()
{
widget.setTitle(’ipsojobs.com widget - setting…’);
UWA.Data.getText(’http://www.ipsojobs.com/api/controller.php?action=zones_sites_list&xsl=netvibes〈=’+getlang(), ipsowidget.zones_sites_processor);
}

La función selectcity, sólo tiene dos lineas, la primera modifica el título del widget widget.setTitle(’ipsojobs.com widget - setting…’); indicando que no estamos en ningúna ciudad sino en la pantalla de setting. La segunda linea carga una url especial de ipsojobs que genera el html necesario para mostrar el seleccionable de las distintas zonas y ciudades UWA.Data.getText(’http://www.ipsojobs.com/api/controller.php?action=zones_sites_list&xsl=netvibes〈=’+getlang(), ipsowidget.zones_sites_processor); con el html resultante se llama a la función ipsowidget.zones_sites_processor que veremos más abajo y simplemente machaca el body con el nuevo html. Cabe destacar que el html contiene la lista de zonas y sus ciudades ocultas, al clicar sobre la zona se llama a la función showhide para mostrar las ciudades de esta zona. Con iGoogle he comprovado que el desplegado y el plegado no funcionan muy bien porque el widget no se redimensiona correctamente, en cambio si arrastras el titulo del widget como si lo fueses a reposicionar, entonces se recalcula la altura del mismo correctamente.

Preferencias y CSSs:


<title>ipsojobs.com widget
<link rel=”icon” type=”image/png” href=”http://www.netvibes.com/favicon.ico” />

<widget:preferences>
<preference name=”url” type=”hidden” label=”URL” defaultValue=”" />
<preference name=”limit” type=”range” label=”Number of items to display” defaultValue=”10″ step=”1″ min=”1″ max=”25″ />
</widget:preferences>

<style type=”text/css”>
/* your CSS rules */

body {
height: 400px;
}

a:hover {
cursor: pointer;
}

h2 {
color: #8CC402;
}

h3 {
color: #8CC402;
}

</style>

Aquí podemos ver que creamos una preferencia llamada url de tipo hidden, es la que nos servira para almacenar la url del recurso RSS de la ciudad seleccionada. También creamos el parametro limit que permitirá al usuario seleccionar el numero de ofertas a mostrar en el rango de 1 a 25.

El meollo de la cuestión:


<script type=”text/javascript”>
<![CDATA[
var ipsowidget = {}

ipsowidget.feed = false;

ipsowidget.display = function(feed)
{
// get the number of items to display
widget.preferences[1].max = feed.items.length;

var toolbar=widget.createElement(’h2′);
var website_url=replace(widget.getValue(’url’), ‘/rss/controller.php’, ”);
toolbar.setHTML(’‘+feed.title+’‘);

var toolbar2=widget.createElement(’h3′);
toolbar2.setHTML(’‘+get_change_city()+’‘);

widget.setTitle(’ipsojobs.com - ‘+feed.title);

// create the ‘ul’ element, applying it the CSS class ‘nv-feedList’
var feedList = widget.createElement(’ul’);

// your ‘ul’ element MUST make use of the ‘nv-feedList’ class
// to ensure your widget uses the UWA UI library
feedList.className = ‘nv-feedList’;

// number of parsed items
var j = 0;

// loop through the downloaded items
for(var i=0; i < feed.items.length; i++)
{
// if the limit is reached, stop looping
if (j >= widget.getValue(’limit’)) break;

// for each item, create the ‘li’ element
var item = feed.items[i];
var li = widget.createElement(’li’);

// create and fill the ‘a’ element of the item with the item’s link
var a = widget.createElement(’a');
a.href = item.link;

// fill the ‘a’ element with the item’s title
var displayTitle = item.title;
a.innerHTML = displayTitle;

// build the title from the 255 first characters of the content
// remove the content’s HTML tags along the way
var title = item.content.stripTags().truncate(255);
a.title = title;

// set a tooltip on the ‘a’ element, with the item’s content
a.onmouseover = function()
{
UWA.Utils.setTooltip(this, this.content, 250);
}

// finally append the ‘a’ element we just filled, into the ‘li’ element
li.appendChild(a);

// …and append the ‘li’ element to the main ‘ul’ element
feedList.appendChild(li);
j++;
}

var general=widget.createElement(’div’);
general.appendChild(toolbar);
general.appendChild(toolbar2);
general.appendChild(feedList);
// once the needed items have been parsed,
// send the main ‘ul’ element to the HTML body tag
widget.setBody(general);
}

ipsowidget.zones_sites_processor = function(text)
{
widget.setBody(text);
}

widget.onLoad = function()
{
if (widget.getValue(’url’)==”)
{
selectcity();
}
else
{
//alert(getlang());
UWA.Data.getFeed(widget.getValue(’url’)+’?lang=’+getlang(), ipsowidget.display);
}
}
]]>
</script>

La mayoría de este código es muy parecido al HOWTO de como hacer un RSS widget con UWA (solucionando algún pequeño bug) con las siguientes diferencias:


var toolbar=widget.createElement(’h2′);
var website_url=replace(widget.getValue(’url’), ‘/rss/controller.php’, ”);
toolbar.setHTML(’‘+feed.title+’‘);

var toolbar2=widget.createElement(’h3′);
toolbar2.setHTML(’‘+get_change_city()+’‘);

widget.setTitle(’ipsojobs.com - ‘+feed.title);

En este fragmento creamos el elemento DOM que nos muestra el nombre de la ciudad seleccionada (con un link a la misma) y el elemento que nos permite cambiar de ciudad. En la última linea también actualizamos la barra de título del widget con el nombre de la ciudad seleccionada.

Al final, hemos añadido los fragmentos DOM creados.

var general=widget.createElement(’div’);
general.appendChild(toolbar);
general.appendChild(toolbar2);
general.appendChild(feedList);
// once the needed items have been parsed,
// send the main ‘ul’ element to the HTML body tag
widget.setBody(general);

El onLoad también esta un poco tuneado, en el ejemplo siempre carga la url, pero en nuestro caso puede ser que el widget se haya instalado por primera vez y no tengamos ninguna url en concreto, por eso hacemos lo siguiente:


widget.onLoad = function()
{
if (widget.getValue(’url’)==”)
{
selectcity();
}
else
{
//alert(getlang());
UWA.Data.getFeed(widget.getValue(’url’)+’?lang=’+getlang(), ipsowidget.display);
}
}

Simplemente detecta si ya tenemos una url fijada y sino llamamos a selectcity que ya se encarga de hacer la llamada AJAX correspondiente para mostrar el selector de ciudades. En caso contrario (este usuario ya tenia la preferencia url fijada para este widget) simplemente la cargamos llamando a UWA.Data.getFeed, fijaros que obtengo el lenguaje del navegador y intento llamar con el lenguaje más apropiado al RSS que me interesa.

Finalmente el body de la página


</head>
<body>
<p>Welcome to the ipsojobs.com widget </p>
<p>…</p>
</body>
</html>

Esta parte no tiene misterio, simplemente mostramos un body, que en seguida se machacará con la función widget.onLoad, que hemos visto más arriba.

Comments

Acuerdo de colaboración con BuscoCanguro.com

Desde ayer lunes 21 de Octubre todos los anuncios de canguros de cualquier ciudad española activa en Ipsojobs.com se publicarán automáticamente en el portal BuscoCanguro.com.

BuscoCanguro.com es un proyecto del Grupo Intercom, especializado en la agregación de anuncios de canguros.

Desde estas líneas expresamos nuestro más sincero agradecimiento por la predisposición y profesionalidad del equipo de BuscoCanguro.com.

Como cierres de este post, una curiosidad: hoy Martes se ha introducido la primera oferta en cirílico en Iposjobs, concretamente en la ciudad bielorrusa de Minsk.

Comments

Cache and GZIP your javascripts and CSSs files to speed up your site

I’ve been trying to mix different sources to optimize the download speed of the javascript files and the CSS files of any web application.

Your Web application can use this files if:
- You can use .htaccess in your hosting
- You have CSS only and JS only folders to apply to the whole folder
- You can use PHP in your hosting

If all the above conditions are met, you can simply add three files to your CSS and the same three files to your JS folder and you are done.

download time

Please, take with caution and do some testing, don’t use directly in a production environment.

Of course, I’ll not give any warranty of success and you have to check the results using the Firebug Extension of Firefox, the “Live HTTP Headers” extension and the YSlow plug in of the Firebug Extension.

Let’s see the three files involved:

First file .htaccess:
Modify your .htaccess file to include the following:


AddHandler application/x-httpd-php .css .js
php_value auto_prepend_file gzip-start.php
php_value auto_append_file gzip-end.php

This code, tells Apache to automatically run the gzip-start.php before the requested javascript or css file and to run the file gzip-end.php after the conclusion of the requested file.

It’s a powerful way to create common headers and footers to pages or files, but be aware that it comes at a cost, it’s more fast to serve the static file alone than to run an php script before and one after the file, of course, you have to do some testing and see if it’s worthy to do so.

Second file, the magical gzip-start.php:

<?php

function get_http_mdate()
{
return gmdate(’D, d M Y H:i:s’, filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF'])).’ GMT’;
}

function check_modified_header()
{// This function is based on code from http://ontosys.com/php/cache.html
$headers=apache_request_headers();
$if_modified_since=preg_replace(’/;.*$/’, ”, $headers['If-Modified-Since']);
if(!$if_modified_since)
{
return;
}

$gmtime=get_http_mdate();

if ($if_modified_since == $gmtime)
{
header(”HTTP/1.1 304 Not Modified”);
exit;
}
}

check_modified_header();

// Open a gzipped buffer
ob_start (”ob_gzhandler”);

if (strpos($_SERVER['PHP_SELF'], ‘.js’)>0)
{// Javascript file
header(”Content-type: text/javascript”);
}
elseif (strpos($_SERVER['PHP_SELF'], ‘.css’)>0)
{// CSS file
header(”Content-type: text/css”);
}

$offset = 60000000; // Far far away in time

// Expires
header(”Expires: “.gmdate(”D, d M Y H:i:s”, time() + $offset) . ” GMT”);

// Cache-Control
header(”Cache-Control: must-revalidate, max-age=”.(time()+$offset));

header(”Last-Modified: “.get_http_mdate());

// generate unique ID, using the modification date and the absolute path to the file
$hash = md5(filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']).$_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']);
header(’Etag: “‘.$hash.’”‘);

?>

The concept is clear, this file is interpreted by the PHP runtime right before the output of the requested file.

Let’s see the different parts of the file:


check_modified_header();

This function looks for the modification date of the requested file (filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF'])) and returns a HTTP 304 (Not Modified) if the file is fresh, to do that, first looks for a request header If-Modified-Since and then compare the dates.


// Open a gzipped buffer
ob_start (”ob_gzhandler”);

if (strpos($_SERVER['PHP_SELF'], ‘.js’)>0)
{// Javascript file
header(”Content-type: text/javascript”);
}
elseif (strpos($_SERVER['PHP_SELF'], ‘.css’)>0)
{// CSS file
header(”Content-type: text/css”);
}

These lines opens a PHP buffer with the trick that it’s using GZip compression, this is a very good feature of PHP that enables you to output gzipped content without any modification. Afterwards we put the Content-Type of the resulting file, depending of the extension of the file we put text/css or text/javascript.


$offset = 60000000; // Far far away in time

// Expires
header(”Expires: “.gmdate(”D, d M Y H:i:s”, time() + $offset) . ” GMT”);

// Cache-Control
header(”Cache-Control: must-revalidate, max-age=”.(time()+$offset));

These lines put the Expires and Cache-Control headers, both far away in time, we create the variable offset with a constant 60 million seconds (about 2 years) and use that to generate a Expires header and a Cache-Control header with the value of two years in the future.

Be warned, your CSS and your JS files will be cached in a lot of proxies and client browsers, you have to use a “rename file policy” in your HTML to prevent the users from using old versions of the files. We’ve got little javascripts and css files to change, but I recommend to split these kind of files in different folders, for instance /js/usually-modified/omatech.js and apply only the caching technique to the folder usually-modified, then rename each time the .js file with a timestamp, say omatech_20071010.js, and change your html code to reflect the new version.


header(”Last-Modified: “.get_http_mdate());

$hash = md5(filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']).$_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']);
header(’Etag: “‘.$hash.’”‘);

The last lines, creates a Last-Modified header, used to indicate the client browser the freshness of the file for future requests, this have interactions with the Cache-Control/max-age and the Expires headers (out of the reach of this post).

Finally we generate an unique Etag header, based on the update time of the requested file and it’s full path.

Third and last file, the not less magical gzip-end.php:


<?php
header(’Content-Length: ‘ . ob_get_length());
ob_end_flush();
?>

This file simply creates dynamically a Content-Length header with the size of the gzipped buffer we have been using for this request, and then flushes it’s contents.

Simple uh ?

I hope you enjoyed and finded useful, feel free to change and play with the code.

I would like to thank the sources where I took inspiration and code:

The tutorial of CSS compression,
Facilitate User Experience with CSS Compression, from Max Kiesler

Good introduction to the problem of speeding up the webpages through caching and zipping javascripts
Serving Javascript Fast

And always a good tutorial even for novices (in Spanish)
¿Por qué y cómo crear un espacio web “cache friendly”? from jcea

Comments (4)

Balance tras un mes de proyecto

Es un poco prematuro sacar conclusiones de un proyecto sólo 4 semanas después de su puesta en marcha pero como mínimo podemos apuntar las sensaciones y las líneas de trabajo abiertas.

Vamos por partes. Un mensaje que recibimos con frequencia es que la idea de ipsojobs gusta. En este sentido nos sentimos satisfechos porque en muy poco tiempo nos hemos posicionado como una referencia en la búsqueda de empleo urgente. Tanto desde la perspectiva del empleador como la del empleado.

Entre todos los correos que hemos recibido, bien pocos nos preguntan sobre la operativa y las pantallas de ipsojobs, por lo que deducimos que el objetivo de crear una herramienta de búsqueda muy simple y sencilla de utilizar también se ha conseguido.

Nuestros dos caballos de batalla en estos momentos son la promoción de la web y la obtención de más contenido. En este sentido estamos trabajando para colaborar con diferentes agregadores de contenido y con páginas que multipliquen el alcance de nuestras ofertas. El primer acuerdo en firme se ha establecido con Trovit, buscador vertical de anuncios clasificados. Mediante este acuerdo, las ofertas introducidas en ipsojobs.com aparecen también en Trovit. En las próximas semanas esperamos anunciar más acuerdos parecidos.

¿Cómo va la promoción de la web?

Cada día el número de ofertas activas augmenta y las visitas únicas al web también. En este sentido damos las gracias a los administradores de cada ciudad por el esfuerzo realizado. Ahora bien, como queda reflejado en la página principal hay mucha diferencia entre ciudades de más o menos el mismo tamaño, así que deberemos trabajarlo porque esta es una clara área de oportunidad.

Algunos de los administradores nos han pedido más herramientas para facilitar la inserción masiva de ofertas ya que han llegado a acuerdos con empresas locales (ETTs o tablones de clasificados) así que recientemente hemos liberado una API que en este momento está en fase de pruebas. Todos los administradores que la quieran utilizar sólo hace falta que se pongan en contacto con nosotros en cities[arroba]ipsojobs.com

Los siguientes temas en desarrollo son un widget para quien quiera pueda mostrar ofertas de ipsojobs en su blog y continuar con la promoción. Este mes tocan los grandes medios tradicionales. Os seguimos informando!

Comments (1)

Integración con Google Maps

En tan solo tres semanas de vida ya hemos cambiado dos veces la página de inicio de www.ipsojobs.com. En la primera versión presentábamos de inicio un mapa mundial con las ciudades activas. Los comentarios de los beta-testers nos hicieron cambiarlo porque despistaba y transmitía un mensaje ambiguo sobre la finalidad de la web.
En la segunda versión apostamos por un listado de ciudades ordenado por zonas geográficas. Con esta alternativa superábamos las ambiguedades generadas en el primer intento, pero perdíamos la representación gráfica de la dispersión geográfica de ipsojobs.

mapa ipsojobs

Así pues la solución intermedia ha sido dejar el mapa a un click. Si el usuario lo desea puede desplegar el mapa por zona desde la página inicial de www.ipsojobs.com (worldwide). Cada ciudad activa se marca con un icono. Al clicar sobre el icono aparece información sobre el total de ofertas activas de la ciudad, la hora de la última oferta introducida y acceso directo a las 5 últimas ofertas publicadas.

Por ahora mantendremos está versión y depuraremos algún pequeño retoque relativo a las traducciones. La otra línea de desarrollo abierta és la programación de una API para la inserción de ofertas y un ‘widget’ para mostrar ofertas de ipsojobs en cualquier web. Esperamos tenerlos acabados en los próximos días para poder dotar del máximo de herramientas de promoción a todos los administradores de ciudades de ipsojobs.

saludos!

Comments (2)

© Omatech