Server Migration

Hi,

after a long time posting, we are announcing that we have just migrated to a new server.

The previous hosting in dreamhost.com was buggy and crashes very often, for that reason we have migrated to a more powerful server and keep improving the service.

Thanks to all of you, users, admins and fans !

Comments

New Trovit USA integration (jobs.trovit.com)

We’re very exited to announce the fully trovit USA integration !
Jobs.trovit.com and ipsojobs.com Integration

Now, the leader in the search of classified ads in Europe (trovit.com) has reached
the US, and ipsojobs.com is providing them one of the best sources of job offers.

From a week ago, the job offers published in ipsojobs.com are searchable too from jobs.trovit.com

Good luck to trovit.com in this new adventure !!!

Comments (2)

New summer features at ipsojobs.com

We’ve proudly upgraded ipsojobs.com and now we’re
publishing the best job offers worldwide via twitter
you can follow our twitter account in http://twitter.com/ipsojobs

twitter ipsojobs.com

How the ipsojobs twitter looks like

And that’s more, some popular cities have their own twitter account, for example:

New York – http://twitter.com/ij_newyork
Chicago – http://twitter.com/ij_chicago
Barcelona – http://twitter.com/ij_barcelona
Madrid – http://twitter.com/ij_madrid
Alicante – http://twitter.com/ij_alicante

and don’t forget the global account:

http://twitter.com/ipsojobs

The twitter integration doesn’t finish here !

You can now easily, post an interesting ipsojobs offer to your twitter account, simply click the
new link “Share in twitter” in any job offer and share it with your friends.

Retweet option in job post

That’s an example of how the retweet link looks like

Comments

ipsojobs.com and oodle.com integration

Ipsojobs.com is proud to annunce the inclusion in the oodle.com search index.

Oodle ipsojobs integration

Oodle ipsojobs integration

Oodle is one of the most important players in the classified ads market globally, this new vertical search engine integration will help Ipsojobs.com to be more popular and more relevant for their users (employeers and jobseekers).

We are receiving visitors from oodle.com since February the 20th (02/20/2009).

Welcome all !

Comments

Nuevo servicio: cursos de formación

Con el propósito firme de mejorar el servicio, presentamos una nueva funcionalidad dirigidad por ahora a los usuarios de España: hemos integrado información sobre cursos de formación en la mayoría de ciudades españolas.

Tanto en el menú principal como en el encabezado de la lista de ofertas, encontramos una nueva opción: “Cursos”. El menú cursos nos dirige a otra página de ipsojobs.com con un listado de cursos de formación clasificados por categorías.

Cursos de formación en ipsojobs.com

Esta integración ha sido posible gracias al acuerdo de colaboración realizado con un nuevo partner: Iberestudios. De momento la oferta de formación sólo esta disponible en ciudades españolas pero no descartamos llegar a nuevos acuerdos para ofrecer una oferta de cursos similar en otros paises en un futuro próximo.

Estamos convencidos que la información sobre cursos se complementa perfectamente con las ofertas laborales y sirve para incrementar la calidad de la experiencia de usuario en ipsojobs.

Deseamos que estas modificaciones sean de vuestro agrado. Por supuesto, agradeceremos cualquier comentario o sugerencia al respecto.

saludos!

Comments (2)

UK expansion

UK tag cloud

December starts with a huge activity in UK cities. Our local administrators are doing a great job there and the number of job listings and the number of visits are increasing day by day. We’d like to thank our UK partners for the great job done there.

On the there side, the development team of IPSOJOBS is working in a very interesting integration in order to complement job listings with some other useful information. We’ve planned to launch the new integration by next week. We keep you updated!

Comments

Ipsojobs 1st Birthday !

It’s been a year since the official launch of ipsojobs.com and we are really proud of the achievements we made in this period.

Let us summarize the major work done in this first year:

Partnerships:

Our major strength rigth now is our partners:  we have city admins who control the content at a city level, partners who send us quality job offers and partners who help us to be more findable in their search engines.

SPAM figthing:

One of our main focus is to assure the quality of all jobs postings. Usually the simplicity of the posting process means lots of spam offers. That’s why  we have developed a multi-level control system to fight spam. Our software and methodology is working really well so we can keep the site clean and with relevant job offers. This give us a clear competitive advantage in front of some similar sites.

The different levels of spam control are:

  1. City administrator, one of the tasks of a city admin is to control the new job offers (usually via RSS) and delete the spam introduced.
  2. The happy anti-spam robot, we clean MLM offers and other spammy job offers with a robot provided with the blessing of Artifial Intelligence, the robot is running around the clock and he never gets tired.
  3. Super Admin filter: we ourselves as super-admins control ALL the new offers in ALL the cities around the globe and filter the bad job offers

Relevance and competition between cities:

We have developed a powerful set of back-office applications to track the performance of all city admins. It allows us to have ‘the pulse’ of Ipsojobs in real time.

We have set some rankings that give to the city admins an idea of their performance related to the other cities.

Even more, we control the relevance of each city in the tagcloud in the worldwide home page, giving more size to the most relevant cities in each area.

SEO:

Every category and every job offer has a relevant and nice URL and the markup is build up to be the most SEO friendly as possible.

We have set links between nearby cities, that allows our job seekers to find jobs in nearby cities and give us automatic cross-linking.

OTHERS:

  • UWA Widget, you can use the UWA widget that allow you to keep informed of the new job offers in your city.
  • Nearby job offers, we have setted an XML API to find jobs nearby a given latitude and longitude.
  • Expiration warnings, when you enter an offer the expiration time is one month, but you can give us your email (optional) to allow you to re-publish the offer for one more month.
  • Google App Engine, we have tried the google app engine as a CDN and blogged about this to share with the community.

We have a steady growth, the platform is robust and the business model is solid. Our mid-term goal is to get strong in a couple more of European countries apart from Spain. We have an unique product, an skilled team, a very positive attitude and the support of more than 150 city ambassadors around the globe. With all these ingredients in place we’re sure we’re heading towards a successful and challenging second year !!!

Thank you all for this great 1st year of Ipsojobs!

Comments (1)

New ipsojobs summer features

The summer is already here, almost in Barcelona, today is better to stay in your workplace with the AC than in the street.

For that reason we’ve been working so hard in new features :)

Expiration Warnings:

We’ve introduced new warnings when your job posting is about to expire. To use this new feature simply introduce a  job posting and then you’ll be asked to fill your contact email.

We’ll send you a link to renew the job posting (if you want) near the end of the publishing period.

When you renew the job posting, you can optionally introduce again an email for a further expiration warning.

Better SEO in Pagination:

The pagination for big cities have become a little bulky, for that reason we’ve simplified the pagination and added a more descriptive title in the links (better than 1, 2, 3 …)

Ultra fast, spam detection for admins:

We’ve set up a new ultra-fast way to remove spammy job postings, we’ll send an email to all the admins telling the secrets of this new feature.

Comments

Out of the cloud

After a few days running a simple CDN in Google App Engine, we’re forced to turn back and wait until Google App Engine are more “mature”.

As you can see in the following image the Google App Engine have got lots of trouble in this June and we cannot afford to lose customers.

For that reason we are now serving again our static content from our servers until the situation in Google App Engine normalizes.

You can follow the google app engine downtimes here

Thank you

Comments

How to create a simple but powerful CDN with Google App Engine (GAE)

The main purpose when I started to look at Google App Engine (3 days ago) was to use it as a “CDN for the rest of us”, a way to cache static content (initially) and have this content distributed along all the infrastructure of Google (maybe the most powerful cloud rigth now)

What we want?:

  • Create a CDN easy to update and free of charge for static resources (images, css, js)
  • Consume as less bandwidth as possible leveraging the If-Modified-Since/Last-Modified/304 Not Modified model

Hands-on:

The first approach, of course, was to look on Google for some help, the post of Andreas Krohn helped a lot to start.

But I want to go further and take care of modern browsers If-Modified-Since requests, then the google framework and a little of Python comes to the rescue.

Note: I’m assuming you’ve already installed the Python environment and the Google App Engine SDK

First of all let me give you two little .bat files that are useful:

Start the test webserver (test.bat):
dev_appserver.py c:\ipsojobscloud

Upload your application to the cloud (update.bat):
appcfg.py update c:\ipsojobscloud

Note: simply change c:\ipsojobscloud for the folder you are working in and contains your app.yaml

Then I’ve setup the app.yaml, it’s very simple (16 lines):

application: ipsojobscloud
version: 1
runtime: python
api_version: 1

handlers:
- url: /favicon.ico
  static_files: favicon.ico
  upload: favicon.ico

- url: /images/favicon.ico
  static_files: favicon.ico
  upload: favicon.ico

- url: /.*
  script: cacheheaders.py

This app.yaml simply tells the GAE the name of the application (ipsojobscloud) the version we’re working on (use only the major release number, GAE automatically takes care of the .x when you upload).

Then we specify two handlers for the favicon.ico static file and a catch-all handler that redirects our requests to the Python script cacheheaders.py

With that environment set, we simply code the cacheheaders.py file, let’s see it in detail:

The skeleton of the file is:

import wsgiref.handlers
from google.appengine.ext import webapp

class MainPage(webapp.RequestHandler):

  def get(self, dir, file, extension):
...

def main():
  application = webapp.WSGIApplication([(r'/(.*)/([^.]*).(.*)', MainPage)], debug=False)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
  main()

Here we are importing the webapp framework and setting the class MainPage, in the main section the only change in the sample GAE is
the regular expression that we used to match our requests, the expression r’/(.*)/([^.]*).(.*)’ is telling that we are using regular expressions (r)
, then take one slash, followed by an arbitray number of characters and another slash /(.*)/ the parentesis tells the regular expression to keep the string beetween the two slashes as a variable. The next part ([^.]*). takes all caracters except a dot and puts them in to the second variable and finally, we’ll take the rest of the input as a variable with (.*)

This regular expression is designed to only capture paths like /images/helloworld.gif where variables are images, helloworld and gif respectively

Note: Of course that’s not a complete solution, we can only have one folder depth, but it’s a good readers exercice to improve that :)

The part that you need to know is that when a request arrives it’s mapped to the get function with the parameters dir, file and extension (and don’t forget the first “self” parameter)

Let’s see the code of the get function in detail:

First, check the validity of the parameters received and set the correct content-type based on the extension:

  def get(self, dir, file, extension):
    if (dir!='js' and dir!='css' and dir!='images'):
      self.error(404)
      return

    if (extension!='js' and extension!='css' and extension!='jpg' and extension!='png' and extension!='gif'):
      self.error(404)
      return

    if extension=='js':
      self.response.headers['Content-Type'] = 'application/x-javascript'
    elif extension=='css':
      self.response.headers['Content-Type'] = 'text/css'
    elif extension=='jpg':
      self.response.headers['Content-Type'] = 'image/jpeg'
    elif extension=='gif':
      self.response.headers['Content-Type'] = 'image/gif'
    elif extension=='png':
      self.response.headers['Content-Type'] = 'image/png'

Note: the firts two ifs are completely optional, we check if the dir variable is in our valid list of dirs (js, css, images) and if the extension of the file is in our allowed list (js, css, jpg, png, gif), you have to change that check or completely remove it at your convenience.

And now the tricky part:

    try:
      import os
      import datetime
      path = dir+'/'+file+"."+extension
      info = os.stat(path)
      lastmod = datetime.datetime.fromtimestamp(info[8])
      if self.request.headers.has_key('If-Modified-Since'):
        dt = self.request.headers.get('If-Modified-Since').split(';')[0]
        modsince = datetime.datetime.strptime(dt, "%a, %d %b %Y %H:%M:%S %Z")
        if modsince >= lastmod:
        # The file is older than the cached copy (or exactly the same)
          self.error(304)
          return
        else:
        # The file is newer
          self.output_file(path, lastmod)
      else:
        self.output_file(path, lastmod)
    except:
      self.error(404)
      return

First we import some packages (os, datetime), then create a variable “path” with the full path of the file we want to retrieve

path = dir+'/'+file+"."+extension

Then, take the info of the file from the Operating System and keep the last modified date into lastmod variable, note that if an error occurs (non existing file for example, the except part will be executed, returning a 404 not found response to the browser).

In the following lines we scan the headers of the request, looking for an If-Modified-Since header, if we found it take the date part

      if self.request.headers.has_key('If-Modified-Since'):
        dt = self.request.headers.get('If-Modified-Since').split(';')[0]
        modsince = datetime.datetime.strptime(dt, "%a, %d %b %Y %H:%M:%S %Z")

Then compare the last modification date of the file against the ifmodifiedsince date and act accordingly, note that self.error(304) will return a response code 304 (Not-Modified) to the browser:

        if modsince >= lastmod:
        # The file is older than the cached copy or the same
          self.error(304)
          return
        else:
        # The file is newer
          self.output_file(path, lastmod)

The self.output_file(path, lastmod) is a function we have defined to avoid code duplication:

  def output_file(self, path, lastmod):
    import datetime
    try:
      self.response.headers['Cache-Control']='public, max-age=31536000'
      self.response.headers['Last-Modified'] = lastmod.strftime("%a, %d %b %Y %H:%M:%S GMT")
      expires=lastmod+datetime.timedelta(days=365)
      self.response.headers['Expires'] = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
      fh=open(path, 'r')
      self.response.out.write(fh.read())
      fh.close
      return
    except IOError:
      self.error(404)
      return

As you can see we imported datetime to manipulate dates and try to do the following:

  • Set the header Cache-Control, to be as much cacheable as posible
  • Set the header Last-Modified (IMPORTANT ! when we send for the first time the file to the browser it keeps the Last-Modified date of the file, this value is the value that will send in the next If-Modified-Since requests, when we usually will respond 304 not-modified!)
  • Calculate an expires date in the future (we’ve put 365 days)
  • Set the Expires header with this value (last-modified+365 days)
  • Open the file and send it to the output and finally close the file
  • return, because when we output the file we’re done

Note: If something happens we returned an standard response of Not Found (404)

Conclusions:

We’ve improved the latency in the requests of static files putting them into the cloud, and keep the bandwidth used in the cloud to a minimum answering correctly to the If-Modified-Since requests and only in about 70 lines of code

One of the advantatges of Google App Engine above Amazon S3 is that GAE is free up 5 million page views a month, that give us a good chance to try this kind of features without spending cash.

You can see the speed improvement on-line in all the ipsojobs.com pages rigth now !

Some screenshots taken from firebug:

First request:

First request (not cached)

Second request:

Second request, cached, note the 304 responses

Detail of a request:

Sample cached response, details

Full source of cacheheaders.py:

import wsgiref.handlers
from google.appengine.ext import webapp

class MainPage(webapp.RequestHandler):

  def output_file(self, path, lastmod):
    import datetime
    try:
      self.response.headers['Cache-Control']='public, max-age=31536000'
      self.response.headers['Last-Modified'] = lastmod.strftime("%a, %d %b %Y %H:%M:%S GMT")
      expires=lastmod+datetime.timedelta(days=365)
      self.response.headers['Expires'] = expires.strftime("%a, %d %b %Y %H:%M:%S GMT")
      fh=open(path, 'r')
      self.response.out.write(fh.read())
      fh.close
      return
    except IOError:
      self.error(404)
      return

  def get(self, dir, file, extension):
    if (dir!='js' and dir!='css' and dir!='images'):
      self.error(404)
      return

    if (extension!='js' and extension!='css' and extension!='jpg' and extension!='png' and extension!='gif'):
      self.error(404)
      return

    if extension=='js':
      self.response.headers['Content-Type'] = 'application/x-javascript'
    elif extension=='css':
      self.response.headers['Content-Type'] = 'text/css'
    elif extension=='jpg':
      self.response.headers['Content-Type'] = 'image/jpeg'
    elif extension=='gif':
      self.response.headers['Content-Type'] = 'image/gif'
    elif extension=='png':
      self.response.headers['Content-Type'] = 'image/png'

    try:
      import os
      import datetime
      path = dir+'/'+file+"."+extension
      info = os.stat(path)
      lastmod = datetime.datetime.fromtimestamp(info[8])
      if self.request.headers.has_key('If-Modified-Since'):
        dt = self.request.headers.get('If-Modified-Since').split(';')[0]
        modsince = datetime.datetime.strptime(dt, "%a, %d %b %Y %H:%M:%S %Z")
        if modsince >= lastmod:
        # The file is older than the cached copy (or exactly the same)
          self.error(304)
          return
        else:
        # The file is newer
          self.output_file(path, lastmod)
      else:
        self.output_file(path, lastmod)
    except:
      self.error(404)
      return

def main():
  application = webapp.WSGIApplication([(r'/(.*)/([^.]*).(.*)', MainPage)], debug=False)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
  main()

Comments (11)

« Previous entries Next Page » Next Page »

© Omatech