<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>IPSOJOBS Blog &#187; ipsojobs return</title>
	<atom:link href="http://www.ipsojobs.com/blog/category/ipsojobs-return/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.ipsojobs.com/blog</link>
	<description>Ipsojobs, when time is of the essence</description>
	<lastBuildDate>Mon, 27 Jul 2009 13:44:42 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.8.2</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Out of the cloud</title>
		<link>http://www.ipsojobs.com/blog/2008/06/26/out-of-the-cloud/</link>
		<comments>http://www.ipsojobs.com/blog/2008/06/26/out-of-the-cloud/#comments</comments>
		<pubDate>Thu, 26 Jun 2008 15:54:46 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[Curiosidades]]></category>
		<category><![CDATA[ipsojobs return]]></category>
		<category><![CDATA[meta ipsojobs]]></category>

		<guid isPermaLink="false">http://www.ipsojobs.com/blog/?p=73</guid>
		<description><![CDATA[After a few days running a simple CDN in Google App Engine, we&#8217;re forced to turn back and wait until Google App Engine are more &#8220;mature&#8221;.
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 [...]]]></description>
			<content:encoded><![CDATA[<p>After a few days running a simple CDN in Google App Engine, we&#8217;re forced to turn back and wait until Google App Engine are more &#8220;mature&#8221;.</p>
<p>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.</p>
<p><a href="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/screenhunter_215.jpg"><img class="aligncenter size-full wp-image-74" title="downtime in google app engine" src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/screenhunter_215.jpg" alt="" width="380" height="312" /></a></p>
<p>For that reason we are now serving again our static content from our servers until the situation in Google App Engine normalizes.</p>
<p>You can follow the <a title="Google appengine downtime notify" href="http://groups.google.com/group/google-appengine-downtime-notify" target="_blank">google app engine downtimes here</a></p>
<p>Thank you</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ipsojobs.com/blog/2008/06/26/out-of-the-cloud/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>How to create a simple but powerful CDN with Google App Engine (GAE)</title>
		<link>http://www.ipsojobs.com/blog/2008/06/17/how-to-create-a-simple-but-powerful-cdn-with-google-app-engine-gae/</link>
		<comments>http://www.ipsojobs.com/blog/2008/06/17/how-to-create-a-simple-but-powerful-cdn-with-google-app-engine-gae/#comments</comments>
		<pubDate>Tue, 17 Jun 2008 09:18:56 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[New features]]></category>
		<category><![CDATA[ipsojobs return]]></category>
		<category><![CDATA[meta ipsojobs]]></category>

		<guid isPermaLink="false">http://www.ipsojobs.com/blog/?p=66</guid>
		<description><![CDATA[The main purpose when I started to look at Google App Engine (3 days ago) was to use it as a &#8220;CDN for the rest of us&#8221;, 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 [...]]]></description>
			<content:encoded><![CDATA[<p><img class="alignleft size-thumbnail wp-image-72" style="float:left" title="google-app-engine2" src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/google-app-engine2-150x150.png" alt="" width="81" height="81" />The main purpose when I started to look at <a href="http://code.google.com/appengine/" target="_blank">Google App Engine</a> (3 days ago) was to use it as a <strong>&#8220;CDN for the rest of us&#8221;</strong>, a way to cache <strong>static content</strong> (initially) and have this content distributed along all the infrastructure of Google (maybe the most powerful cloud rigth now)</p>
<p><strong>What we want?:</strong></p>
<ul>
<li>Create a CDN easy to update and free of charge for static resources (images, css, js)</li>
<li>Consume as less bandwidth as possible leveraging the If-Modified-Since/Last-Modified/304 Not Modified model</li>
</ul>
<p><strong>Hands-on:</strong></p>
<p>The first approach, of course, was to look on Google for some help, the post of <a href="http://www.digitalistic.com/2008/06/09/10-easy-steps-to-use-google-app-engine-as-your-own-cdn/" target="_blank">Andreas Krohn</a> helped a lot to start.</p>
<p>But I want to go further and take care of modern browsers <strong>If-Modified-Since requests</strong>, then the google framework and a little of <strong>Python</strong> comes to the rescue.</p>
<p><em>Note: I&#8217;m assuming you&#8217;ve already installed the Python environment and the Google App Engine SDK</em></p>
<p>First of all let me give you two little .bat files that are useful:</p>
<p>Start the test webserver (<strong>test.bat</strong>):<br />
<strong>dev_appserver.py c:\ipsojobscloud</strong></p>
<p>Upload your application to the cloud (<strong>update.bat</strong>):<br />
<strong>appcfg.py update c:\ipsojobscloud</strong></p>
<p><em>Note: simply change c:\ipsojobscloud for the folder you are working in and contains your app.yaml</em></p>
<p>Then I&#8217;ve setup the <strong>app.yaml</strong>, it&#8217;s very simple (16 lines):</p>
<pre>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</pre>
<p>This app.yaml simply tells the GAE the name of the application (ipsojobscloud) the version we&#8217;re working on (use only the major release number, GAE automatically takes care of the .x when you upload).</p>
<p>Then we specify two handlers for the favicon.ico static file and a catch-all handler that redirects our requests to the <strong>Python script cacheheaders.py</strong></p>
<p>With that environment set, we simply code the cacheheaders.py file, let&#8217;s see it in detail:</p>
<p>The skeleton of the file is:</p>
<pre>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()</pre>
<p>Here we are importing the webapp framework and setting the class MainPage, in the main section the only change in the sample GAE is<br />
the regular expression that we used to match our requests, the expression <strong>r&#8217;/(.*)/([^.]*).(.*)&#8217;</strong> is telling that we are using regular expressions (r)<br />
, 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&#8217;ll take the rest of the input as a variable with (.*)</p>
<p>This regular expression is designed to <strong>only capture paths like /images/helloworld.gif</strong> where variables are <strong>images, helloworld and gif</strong> respectively</p>
<p><em>Note: Of course that&#8217;s not a complete solution, we can only have one folder depth, but it&#8217;s a good readers exercice to improve that  <img src='http://www.ipsojobs.com/blog/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' /> </em></p>
<p>The part that you need to know is that when a request arrives it&#8217;s mapped to the get function with the parameters dir, file and extension (and don&#8217;t forget the first &#8220;self&#8221; parameter)</p>
<p><strong>Let&#8217;s see the code of the get function in detail:</strong></p>
<p>First, check the validity of the parameters received and set the correct content-type based on the extension:</p>
<pre>  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'</pre>
<p><em>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.<br />
</em></p>
<p><strong>And now the tricky part:<br />
</strong></p>
<pre>    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 &gt;= 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</pre>
<p>First we import some packages (os, datetime), then create a variable &#8220;path&#8221; with the full path of the file we want to retrieve</p>
<pre>path = dir+'/'+file+"."+extension</pre>
<p>Then, take the info of the file from the Operating System and keep the <strong>last modified date</strong> 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).</p>
<p>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</p>
<pre>      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")</pre>
<p>Then <strong>compare the last modification date of the file against the ifmodifiedsince date</strong> and act accordingly, note that self.error(304) will return a response code 304 (Not-Modified) to the browser:</p>
<pre>        if modsince &gt;= 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)</pre>
<p>The <strong>self.output_file(path, lastmod)</strong> is a function we have defined to avoid code duplication:</p>
<pre>  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</pre>
<p>As you can see we imported datetime to manipulate dates and try to do the following:</p>
<ul>
<li>Set the header Cache-Control, to be as much cacheable as posible</li>
<li>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!)</li>
<li>Calculate an expires date in the future (we&#8217;ve put 365 days)</li>
<li>Set the Expires header with this value (last-modified+365 days)</li>
<li>Open the file and send it to the output and finally close the file</li>
<li>return, because when we output the file we&#8217;re done</li>
</ul>
<p><em>Note: If something happens we returned an standard response of Not Found (404)</em></p>
<p><strong>Conclusions:</strong></p>
<blockquote><p>We&#8217;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 <strong>in about 70 lines of code</strong></p></blockquote>
<p>One of the advantatges of Google App Engine above Amazon S3 is that GAE <strong>is free up 5 million page views a month</strong>, that give us a good chance to try this kind of features without spending cash.</p>
<p>You can see the speed improvement on-line in all the <a href="http://www.ipsojobs.com/" target="_blank">ipsojobs.com</a> pages rigth now !</p>
<p><strong>Some screenshots taken from firebug:</strong></p>
<p><strong>First request:</strong></p>
<p><a href="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/first_request_500.jpg"><img class="aligncenter size-full wp-image-67" title="first_request_500" src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/first_request_500.jpg" alt="First request (not cached)" width="500" height="193" /></a></p>
<p><strong>Second request:</strong></p>
<p><a href="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/second_request_500.jpg"><img class="aligncenter size-full wp-image-68" title="second_request_500" src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/second_request_500.jpg" alt="Second request, cached, note the 304 responses" width="500" height="209" /></a></p>
<p><strong>Detail of a request:</strong></p>
<p><a href="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/sample_cache_friendly_response_500.jpg"><img class="aligncenter size-full wp-image-69" title="sample_cache_friendly_response_500" src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/sample_cache_friendly_response_500.jpg" alt="Sample cached response, details" width="500" height="226" /></a></p>
<p><strong>Full source of cacheheaders.py:</strong></p>
<pre>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 &gt;= 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()</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.ipsojobs.com/blog/2008/06/17/how-to-create-a-simple-but-powerful-cdn-with-google-app-engine-gae/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>Put the latest job offers in your web site, the easy way</title>
		<link>http://www.ipsojobs.com/blog/2008/06/10/put-the-latest-job-offers-in-your-web-the-easy-way/</link>
		<comments>http://www.ipsojobs.com/blog/2008/06/10/put-the-latest-job-offers-in-your-web-the-easy-way/#comments</comments>
		<pubDate>Tue, 10 Jun 2008 16:16:16 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[ipsojobs return]]></category>
		<category><![CDATA[meta ipsojobs]]></category>

		<guid isPermaLink="false">http://www.ipsojobs.com/blog/?p=62</guid>
		<description><![CDATA[Again, google&#8217;s folks come out with a great and simple service, it&#8217;s called:
Google AJAX Feed API

The idea is simple, just put some keywords, google will look for relevant RSS feeds and generate a simple but elegant RSS slideshow with the latest &#8220;news&#8221; contained in those feeds.
How can help you to put the latest job offers [...]]]></description>
			<content:encoded><![CDATA[<p>Again, google&#8217;s folks come out with a great and simple service, it&#8217;s called:</p>
<p><a href="http://www.google.com/uds/solutions/wizards/dynamicfeed.html" target="_blank">Google AJAX Feed API</a></p>
<p><a href="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/screenhunter_212.jpg"><img class="alignleft size-full wp-image-65" title="screenhunter_212" src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/screenhunter_212.jpg" alt="That\'s what we want to obtain" width="352" height="313" /></a></p>
<p>The idea is simple, just put some keywords, google will look for relevant RSS feeds and generate a simple but elegant RSS slideshow with the latest &#8220;news&#8221; contained in those feeds.</p>
<p>How can help you to put the <a href="http://www.ipsojobs.com" target="_blank">latest job offers</a> of ipsojobs.com in your website?</p>
<p><strong>EASY</strong><br />
Just go to  <a href="http://www.google.com/uds/solutions/wizards/dynamicfeed.html" target="_blank">http://www.google.com/uds/solutions/wizards/dynamicfeed.html</a></p>
<p>In the Style, check the best suited for your site (if you choose &#8220;Vertical Stacked&#8221; be sure to choose an appropiate title too)</p>
<p>In the <strong>&#8220;Feeds Expression&#8221;</strong> enter your favorite ipsojobs sites, for example:</p>
<blockquote><p>ipsojobs barcelona, ipsojobs sabadell</p></blockquote>
<p>The comma, separates the cities and it&#8217;s important to put ipsojobs every time to be sure that google takes the correct RSS</p>
<p>When you click the Preview button be sure that in the &#8220;Direct Feed URL&#8221; are appearing valid ipsojobs RSS feeds, like in the example:</p>
<blockquote><p>ipsojobs barcelona: http://barcelona.ipsojobs.com/rss/controller.php<br />
ipsojobs sabadell: http://sabadell.ipsojobs.com/rss/controller.php</p></blockquote>
<p>Finally, go to Generate Code and copy and paste it in to <strong>your favorite CMS</strong>.</p>
<p><a href="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/google_ajax_api.jpg"><img class="aligncenter size-full wp-image-63" title="google_ajax_api" src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/google_ajax_api.jpg" alt="The sample, visually" width="500" height="239" /></a></p>
<p><strong>That&#8217;s what we have done</strong></p>
<p><a href="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/google_ajax_api_integrat.jpg"><img class="aligncenter size-full wp-image-64" title="google_ajax_api_integrat" src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/06/google_ajax_api_integrat.jpg" alt="The example integrated in a Blog" width="500" height="367" /></a></p>
<p><strong>Sample integration in a blogger page</strong></p>
<p><!-- ++Begin Dynamic Feed Wizard Generated Code++ --></p>
]]></content:encoded>
			<wfw:commentRss>http://www.ipsojobs.com/blog/2008/06/10/put-the-latest-job-offers-in-your-web-the-easy-way/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>New IPSOJOBS API for developers and geocoders</title>
		<link>http://www.ipsojobs.com/blog/2008/02/07/new-ipsojobs-api-for-developers-and-geocoders/</link>
		<comments>http://www.ipsojobs.com/blog/2008/02/07/new-ipsojobs-api-for-developers-and-geocoders/#comments</comments>
		<pubDate>Thu, 07 Feb 2008 12:27:12 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[New features]]></category>
		<category><![CDATA[ipsojobs return]]></category>
		<category><![CDATA[meta ipsojobs]]></category>

		<guid isPermaLink="false">http://www.ipsojobs.com/blog/2008/02/07/new-ipsojobs-api-for-developers-and-geocoders/</guid>
		<description><![CDATA[We&#8217;ve just launched a new API for all the developers/geocoders around de world.
As you may know Ipsojobs.com is a global job bank, fast easy and free for everybody !
The new API allows you to locate job offers near you, you only need to give us your latitude and longitude and we will return you a [...]]]></description>
			<content:encoded><![CDATA[<p>We&#8217;ve just launched a <strong>new API</strong> for all the developers/geocoders around de world.</p>
<p>As you may know Ipsojobs.com is a global job bank, fast easy and free for everybody !</p>
<p><strong>The new API allows you to locate job offers near you</strong>, you only need to give us your latitude and longitude and we will return you a nice XML containing the nearest Ipsojobs cities and the last job postings in that city (if any).</p>
<p><a href="http://www.ipsojobs.com/blog/wp-content/uploads/2008/02/screenhunter_146.jpg" title="XML of the new API"><img src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/02/screenhunter_146.jpg" title="XML of the new API" alt="XML of the new API" align="bottom" /></a></p>
<p>You can see a couple of examples:<br />
<a href="http://www.ipsojobs.com/api/nearby.php?lat=41.387917&amp;lng=2.169918" target="_blank">Jobs near Barcelona</a><br />
<a href="http://www.ipsojobs.com/api/nearby.php?lat=40.416706&amp;lng=-3.703270" target="_blank">Jobs near Madrid</a></p>
<p>As you may see in the examples the XML structure is quite <strong>simple</strong>:</p>
<p><strong>&lt;cities&gt;<br />
&lt;city&gt;</strong><br />
<strong> &lt;home&gt;</strong>URL of the Ipsojobs city home<strong>&lt;/home&gt;</strong><br />
<strong> &lt;rss&gt; </strong>URL of the RSS of that city<strong>&lt;/rss&gt;</strong><br />
<strong> &lt;title&gt;</strong>Title of the city, for example &#8220;Badalona, Spain&#8221;<strong>&lt;/title&gt;</strong><br />
<strong> &lt;current_posts&gt;</strong>Number of posts active in that city<strong>&lt;/current_posts&gt;</strong><br />
<strong> &lt;lat&gt;</strong>Latitude of the city<strong>&lt;/lat&gt;</strong><br />
<strong> &lt;lng&gt;</strong>Longitud of the city<strong>&lt;/lng&gt;</strong><br />
<strong> &lt;posts&gt;</strong><br />
<strong> &lt;post&gt;</strong><br />
<strong> &lt;title&gt;</strong>Title of the job posting<strong>&lt;/title&gt;</strong><br />
<strong> &lt;urlnice&gt;</strong>URL of the job posting<strong>&lt;/urlnice&gt;</strong><br />
<strong> &lt;/post&gt;</strong><br />
&#8230;<br />
<strong> &lt;/posts&gt;</strong><br />
<strong> &lt;/city&gt;</strong><br />
&#8230;<br />
<strong> &lt;/cities&gt;</strong></p>
<p>You can use the API, just pointing to the URL  <strong>http://www.ipsojobs.com/api/nearby.php</strong> with two parameters, lat and lng, as in this example.</p>
<p><a href="http://www.ipsojobs.com/api/nearby.php?lat=40.416706&amp;lng=-3.703270" target="_blank">http://www.ipsojobs.com/api/nearby.php?lat=40.416706&amp;lng=-3.703270</a></p>
<p>If you think this API is nonsense, we&#8217;ve already our <strong>FIRST IMPLEMENTATION</strong>, check this site:</p>
<p><a href="http://www.esofid.com/"><strong>http://www.esofid.com/</strong></a></p>
<p>This is an organization of <strong>&#8220;Language Schools&#8221;</strong> all around Spain, if you search in the top box, look for &#8220;Barcelona&#8221; for example, you will see all the <strong>&#8220;Official Language Schools in that area&#8221;</strong>, then if you click on one of them, you will see three tabs, the first are the basic info of the School, but the second contains a Panoramio&#8217;s picture integration and the Jobs near that location with the help of the API we&#8217;ve just launched, see the picture.</p>
<p><a href="http://www.ipsojobs.com/blog/wp-content/uploads/2008/02/ipsoblog.jpg" title="Esofid, Language Schools Integrated with Ipsojobs API"><img src="http://www.ipsojobs.com/blog/wp-content/uploads/2008/02/ipsoblog.jpg" alt="Esofid, Language Schools Integrated with Ipsojobs API" /></a></p>
<p>Nice, uh ?</p>
<p><strong>Thanks to Raúl </strong>for the idea and the first integration !</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ipsojobs.com/blog/2008/02/07/new-ipsojobs-api-for-developers-and-geocoders/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Nuevo widget UWA de ipsojobs.com</title>
		<link>http://www.ipsojobs.com/blog/2007/10/29/nuevo-widget-uwa-de-ipsojobscom/</link>
		<comments>http://www.ipsojobs.com/blog/2007/10/29/nuevo-widget-uwa-de-ipsojobscom/#comments</comments>
		<pubDate>Mon, 29 Oct 2007 14:01:32 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[New features]]></category>
		<category><![CDATA[ipsojobs return]]></category>

		<guid isPermaLink="false">http://www.ipsojobs.com/blog/2007/10/29/nuevo-widget-uwa-de-ipsojobscom/</guid>
		<description><![CDATA[Esta semana hemos estado preparando un widget UWA en ipsojobs, UWA significa &#8220;Universal Widget API&#8221; y es una especie de standard creado por netvibes para poder crear Widgets en cualquier plataforma (Netvibes, igoogle, opera, iPhone) una especie de &#8220;write once, run everywhere&#8221; que prometia Sun con Java en su momento pero aplicado al mundo de [...]]]></description>
			<content:encoded><![CDATA[<p>Esta semana hemos estado preparando un widget <a href="http://dev.netvibes.com/" target="_blank">UWA</a> en ipsojobs, UWA significa <a href="http://dev.netvibes.com/" target="_blank">&#8220;Universal Widget API&#8221;</a> y es una especie de standard creado por netvibes para poder crear Widgets en cualquier plataforma (Netvibes, igoogle, opera, iPhone) una especie de &#8220;write once, run everywhere&#8221; que prometia Sun con Java en su momento pero aplicado al mundo de los Widgets.</p>
<p>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.</p>
<p><a href="http://www.ipsojobs.com/blog/2007/10/29/nuevo-widget-uwa-de-ipsojobscom/ipsojobs-uwa-widget-in-action/" target="_blank" rel="attachment wp-att-16" title="Ipsojobs UWA Widget in action"><img src="http://www.ipsojobs.com/blog/wp-content/uploads/2007/10/ipsojobs_uwa_widget.jpg" alt="Ipsojobs UWA Widget in action" /></a></p>
<p>El widget pretende ser algo un poco más complejo que un simple lector de RSS, en concreto los objetivos del widget son:</p>
<ul>
<li>Permitir seleccionar fácilmente la ciudad sobre la que queremos consultar las ofertas de trabajo de ipsojobs.</li>
<li>La selección de la ciudad debe ser &#8220;dinámica&#8221; dado que cada día aparecen ciudades nuevas no puede ser una lista estática.</li>
<li>Cuando estas viendo las ofertas de una ciudad siempre podrás cambiar a otra ciudad fácilmente.</li>
<li>El número de ofertas por ciudad debe ser personalizable.</li>
<li>Una vez seleccionada una ciudad el widget debe &#8220;recordar la preferencia&#8221; y mostrar las ofertas de la ciudad hasta que el usuario decida cambiarla.</li>
</ul>
<p>Si quereis probarlo simplemente podéis visitar <a href="http://www.ipsojobs.com/widgets/uwa.html" target="_blank">http://www.ipsojobs.com/widgets/uwa.html</a></p>
<p>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.</p>
<p>Declaración:<br />
<strong><br />
&lt;?xml version=&#8221;1.0&#8243; encoding=&#8221;utf-8&#8243;?&gt;<br />
&lt;!DOCTYPE html PUBLIC &#8220;-//W3C//DTD XHTML 1.0 Strict//EN&#8221; &#8220;http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd&#8221;&gt;<br />
&lt;html xmlns=&#8221;http://www.w3.org/1999/xhtml&#8221; xmlns:widget=&#8221;http://www.netvibes.com/ns/&#8221; &gt;<br />
&lt;head&gt;</strong></p>
<p><strong>&lt;meta name=&#8221;author&#8221; content=&#8221;Agusti Pons, omatech.com&#8221; /&gt;<br />
&lt;meta name=&#8221;description&#8221; content=&#8221;ipsojobs.com find jobs easy in your city&#8221; /&gt;</strong></p>
<p><strong>&lt;meta name=&#8221;apiVersion&#8221; content=&#8221;1.0&#8243; /&gt;<br />
&lt;meta name=&#8221;autoRefresh&#8221; content=&#8221;20&#8243; /&gt;<br />
&lt;meta name=&#8221;debugMode&#8221; content=&#8221;true&#8221; /&gt;</strong></p>
<p><strong>&lt;link rel=&#8221;stylesheet&#8221; type=&#8221;text/css&#8221; href=&#8221;http://www.netvibes.com/themes/uwa/style.css&#8221; /&gt;<br />
&lt;script type=&#8221;text/javascript&#8221; src=&#8221;http://www.netvibes.com/js/UWA/load.js.php?env=Standalone&#8221;&gt;</strong></p>
<p>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.</p>
<p>Javascripts útiles:</p>
<p><strong><br />
&lt;script type=&#8221;text/javascript&#8221;&gt;<br />
&lt;![CDATA[<br />
function showhide(e)<br />
{<br />
//el = document.getElementById(e);<br />
el = widget.body.getElementsByClassName(e)[0];<br />
el.style.display = el.style.display == &#8220;block&#8221; ? &#8220;none&#8221; : &#8220;block&#8221;;<br />
}</strong></p>
<p><strong>function getlang()<br />
{<br />
return widget.lang.substr(0, 2);<br />
}</strong></p>
<p><strong>function link(url)<br />
{<br />
if (widget &amp;&amp; widget.openURL)<br />
{<br />
widget.openURL(url);<br />
}<br />
}</strong></p>
<p><strong>function getSelected(ctrl)<br />
{<br />
for(i=0;i<ctrl.length;i++)><br />
{<br />
if(ctrl[i].checked)<br />
{<br />
return ctrl[i].value;<br />
}<br />
}<br />
return &#8221;;<br />
}</ctrl.length;i++)></strong></p>
<p><strong>function setcity(url)<br />
{<br />
widget.setBody(&#8217;&lt;p&gt;&#8230;&lt;/p&gt;&#8217;);<br />
widget.setValue(&#8217;url&#8217;, url);<br />
UWA.Data.getFeed(widget.getValue(&#8217;url&#8217;)+&#8217;?lang=&#8217;+getlang(), ipsowidget.display);<br />
}</strong></p>
<p><strong>function selectcity()<br />
{<br />
widget.setTitle(&#8217;ipsojobs.com widget &#8211; setting&#8230;&#8217;);<br />
UWA.Data.getText(&#8217;http://www.ipsojobs.com/api/controller.php?action=zones_sites_list&amp;xsl=netvibes〈=&#8217;+getlang(), ipsowidget.zones_sites_processor);<br />
}</strong></p>
<p><strong>function replace(texto,s1,s2)<br />
{<br />
return texto.split(s1).join(s2);<br />
}</strong></p>
<p><strong>function get_change_city()<br />
{<br />
var lang = getlang();<br />
if (lang==&#8217;es&#8217;)<br />
{<br />
return &#8216;Cambiar ciudad&#8217;;<br />
}<br />
if (lang==&#8217;ca&#8217;)<br />
{<br />
return &#8216;Canviar ciutat&#8217;;<br />
}<br />
if (lang==&#8217;fr&#8217;)<br />
{<br />
return &#8216;Changer de ville&#8217;;<br />
}<br />
else<br />
{<br />
return &#8216;Select city&#8217;;<br />
}<br />
}<br />
]]&gt;<br />
&lt;/script&gt;</strong></p>
<p>Las dos funciones más interesantes son setcity() y selectcity() que detallo a continuación:</p>
<p><strong><br />
function setcity(url)<br />
{<br />
widget.setBody(&#8217;&lt;p&gt;&#8230;&lt;/p&gt;&#8217;);<br />
widget.setValue(&#8217;url&#8217;, url);<br />
UWA.Data.getFeed(widget.getValue(&#8217;url&#8217;)+&#8217;?lang=&#8217;+getlang(), ipsowidget.display);<br />
}<br />
</strong></p>
<p>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(&#8217;&lt;p&gt;&#8230;&lt;/p&gt;&#8217;);) 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(&#8217;url&#8217;, 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(&#8217;url&#8217;)+&#8217;?lang=&#8217;+getlang(), ipsowidget.display);.</p>
<p>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 <a href="http://dev.netvibes.com/doc/howto/build_a_rss_reader" target="_blank">HOWTO de como mostrar un RSS en la web de Netvibes</a>.</p>
<p><strong><br />
function selectcity()<br />
{<br />
widget.setTitle(&#8217;ipsojobs.com widget &#8211; setting&#8230;&#8217;);<br />
UWA.Data.getText(&#8217;http://www.ipsojobs.com/api/controller.php?action=zones_sites_list&amp;xsl=netvibes〈=&#8217;+getlang(), ipsowidget.zones_sites_processor);<br />
}<br />
</strong></p>
<p>La función selectcity, sólo tiene dos lineas, la primera modifica el título del widget widget.setTitle(&#8217;ipsojobs.com widget &#8211; setting&#8230;&#8217;); 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(&#8217;http://www.ipsojobs.com/api/controller.php?action=zones_sites_list&amp;xsl=netvibes〈=&#8217;+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.</p>
<p>Preferencias y CSSs:</p>
<p><strong><br />
&lt;title&gt;ipsojobs.com widget<br />
&lt;link rel=&#8221;icon&#8221; type=&#8221;image/png&#8221; href=&#8221;http://www.netvibes.com/favicon.ico&#8221; /&gt;</strong></p>
<p><strong>&lt;widget:preferences&gt;<br />
&lt;preference name=&#8221;url&#8221; type=&#8221;hidden&#8221; label=&#8221;URL&#8221; defaultValue=&#8221;" /&gt;<br />
&lt;preference name=&#8221;limit&#8221; type=&#8221;range&#8221; label=&#8221;Number of items to display&#8221; defaultValue=&#8221;10&#8243; step=&#8221;1&#8243; min=&#8221;1&#8243; max=&#8221;25&#8243; /&gt;<br />
&lt;/widget:preferences&gt;</strong></p>
<p><strong>&lt;style type=&#8221;text/css&#8221;&gt;<br />
/* your CSS rules */</strong></p>
<p><strong>body {<br />
height: 400px;<br />
}</strong></p>
<p><strong>a:hover {<br />
cursor: pointer;<br />
}</strong></p>
<p><strong>h2 {<br />
color: #8CC402;<br />
}</strong></p>
<p><strong>h3 {<br />
color: #8CC402;<br />
}</strong></p>
<p><strong>&lt;/style&gt;</strong></p>
<p>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.</p>
<p>El meollo de la cuestión:</p>
<p><strong><br />
&lt;script type=&#8221;text/javascript&#8221;&gt;<br />
&lt;![CDATA[<br />
var ipsowidget = {}</strong></p>
<p><strong>ipsowidget.feed = false;</strong></p>
<p><strong>ipsowidget.display = function(feed)<br />
{<br />
// get the number of items to display<br />
widget.preferences[1].max = feed.items.length;</strong></p>
<p><strong>var toolbar=widget.createElement(&#8217;h2&#8242;);<br />
var website_url=replace(widget.getValue(&#8217;url&#8217;), &#8216;/rss/controller.php&#8217;, &#8221;);<br />
toolbar.setHTML(&#8217;<a onclick="link(\''+website_url+'\')">&#8216;+feed.title+&#8217;</a>&#8216;);</strong></p>
<p><strong>var toolbar2=widget.createElement(&#8217;h3&#8242;);<br />
toolbar2.setHTML(&#8217;<a onclick="selectcity();">&#8216;+get_change_city()+&#8217;</a>&#8216;);</strong></p>
<p><strong>widget.setTitle(&#8217;ipsojobs.com &#8211; &#8216;+feed.title);</strong></p>
<p><strong>// create the &#8216;ul&#8217; element, applying it the CSS class &#8216;nv-feedList&#8217;<br />
var feedList = widget.createElement(&#8217;ul&#8217;);</strong></p>
<p><strong>// your &#8216;ul&#8217; element MUST make use of the &#8216;nv-feedList&#8217; class<br />
// to ensure your widget uses the UWA UI library<br />
feedList.className = &#8216;nv-feedList&#8217;;</strong></p>
<p><strong>// number of parsed items<br />
var j = 0;</strong></p>
<p><strong>// loop through the downloaded items<br />
for(var i=0; i &lt; feed.items.length; i++)<br />
{<br />
// if the limit is reached, stop looping<br />
if (j &gt;= widget.getValue(&#8217;limit&#8217;)) break;</strong></p>
<p><strong>// for each item, create the &#8216;li&#8217; element<br />
var item = feed.items[i];<br />
var li = widget.createElement(&#8217;li&#8217;);</strong></p>
<p><strong>// create and fill the &#8216;a&#8217; element of the item with the item&#8217;s link<br />
var a = widget.createElement(&#8217;a');<br />
a.href = item.link;</strong></p>
<p><strong>// fill the &#8216;a&#8217; element with the item&#8217;s title<br />
var displayTitle = item.title;<br />
a.innerHTML = displayTitle;</strong></p>
<p><strong>// build the title from the 255 first characters of the content<br />
// remove the content&#8217;s HTML tags along the way<br />
var title = item.content.stripTags().truncate(255);<br />
a.title = title;</strong></p>
<p><strong>// set a tooltip on the &#8216;a&#8217; element, with the item&#8217;s content<br />
a.onmouseover = function()<br />
{<br />
UWA.Utils.setTooltip(this, this.content, 250);<br />
}</strong></p>
<p><strong>// finally append the &#8216;a&#8217; element we just filled, into the &#8216;li&#8217; element<br />
li.appendChild(a);</strong></p>
<p><strong>// &#8230;and append the &#8216;li&#8217; element to the main &#8216;ul&#8217; element<br />
feedList.appendChild(li);<br />
j++;<br />
}</strong></p>
<p><strong>var general=widget.createElement(&#8217;div&#8217;);<br />
general.appendChild(toolbar);<br />
general.appendChild(toolbar2);<br />
general.appendChild(feedList);<br />
// once the needed items have been parsed,<br />
// send the main &#8216;ul&#8217; element to the HTML body tag<br />
widget.setBody(general);<br />
}</strong></p>
<p><strong>ipsowidget.zones_sites_processor = function(text)<br />
{<br />
widget.setBody(text);<br />
}</strong></p>
<p><strong>widget.onLoad = function()<br />
{<br />
if (widget.getValue(&#8217;url&#8217;)==&#8221;)<br />
{<br />
selectcity();<br />
}<br />
else<br />
{<br />
//alert(getlang());<br />
UWA.Data.getFeed(widget.getValue(&#8217;url&#8217;)+&#8217;?lang=&#8217;+getlang(), ipsowidget.display);<br />
}<br />
}<br />
]]&gt;<br />
&lt;/script&gt;<br />
</strong></p>
<p>La mayoría de este código es muy parecido al <a href="http://dev.netvibes.com/doc/howto/build_a_rss_reader" target="_blank">HOWTO de como hacer un RSS widget con UWA</a> (solucionando algún pequeño bug) con las siguientes diferencias:</p>
<p><strong><br />
var toolbar=widget.createElement(&#8217;h2&#8242;);<br />
var website_url=replace(widget.getValue(&#8217;url&#8217;), &#8216;/rss/controller.php&#8217;, &#8221;);<br />
toolbar.setHTML(&#8217;<a onclick="link(\''+website_url+'\')">&#8216;+feed.title+&#8217;</a>&#8216;);</strong></p>
<p><strong>var toolbar2=widget.createElement(&#8217;h3&#8242;);<br />
toolbar2.setHTML(&#8217;<a onclick="selectcity();">&#8216;+get_change_city()+&#8217;</a>&#8216;);</strong></p>
<p><strong>widget.setTitle(&#8217;ipsojobs.com &#8211; &#8216;+feed.title);<br />
</strong></p>
<p>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.</p>
<p>Al final, hemos añadido los fragmentos DOM creados.<br />
<strong><br />
var general=widget.createElement(&#8217;div&#8217;);<br />
general.appendChild(toolbar);<br />
general.appendChild(toolbar2);<br />
general.appendChild(feedList);<br />
// once the needed items have been parsed,<br />
// send the main &#8216;ul&#8217; element to the HTML body tag<br />
widget.setBody(general);<br />
</strong></p>
<p>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:</p>
<p><strong><br />
widget.onLoad = function()<br />
{<br />
if (widget.getValue(&#8217;url&#8217;)==&#8221;)<br />
{<br />
selectcity();<br />
}<br />
else<br />
{<br />
//alert(getlang());<br />
UWA.Data.getFeed(widget.getValue(&#8217;url&#8217;)+&#8217;?lang=&#8217;+getlang(), ipsowidget.display);<br />
}<br />
}<br />
</strong></p>
<p>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.</p>
<p>Finalmente el body de la página</p>
<p><strong><br />
&lt;/head&gt;<br />
&lt;body&gt;<br />
&lt;p&gt;Welcome to the ipsojobs.com widget &lt;/p&gt;<br />
&lt;p&gt;&#8230;&lt;/p&gt;<br />
&lt;/body&gt;<br />
&lt;/html&gt;<br />
</strong></p>
<p>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.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ipsojobs.com/blog/2007/10/29/nuevo-widget-uwa-de-ipsojobscom/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Cache and GZIP your javascripts and CSSs files to speed up your site</title>
		<link>http://www.ipsojobs.com/blog/2007/10/15/cache-and-gzip-your-javascripts-and-csss-files-to-speed-up-your-site/</link>
		<comments>http://www.ipsojobs.com/blog/2007/10/15/cache-and-gzip-your-javascripts-and-csss-files-to-speed-up-your-site/#comments</comments>
		<pubDate>Mon, 15 Oct 2007 08:31:58 +0000</pubDate>
		<dc:creator>Omatech</dc:creator>
				<category><![CDATA[ipsojobs return]]></category>

		<guid isPermaLink="false">http://www.ipsojobs.com/blog/2007/10/15/cache-and-gzip-your-javascripts-and-csss-files-to-speed-up-your-site/</guid>
		<description><![CDATA[I&#8217;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 [...]]]></description>
			<content:encoded><![CDATA[<p>I&#8217;ve been trying to mix different sources to optimize the download speed of the javascript files and the CSS files of any web application.</p>
<p>Your Web application can use this files if:<br />
- You can use .htaccess in your hosting<br />
- You have CSS only and JS only folders to apply to the whole folder<br />
- You can use PHP in your hosting</p>
<p>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.</p>
<p><a href="http://www.ipsojobs.com/blog/2007/10/15/cache-and-gzip-your-javascripts-and-csss-files-to-speed-up-your-site/download-time/" rel="attachment wp-att-13" title="download time"><img src="http://www.ipsojobs.com/blog/wp-content/uploads/2007/10/screenhunter_071.jpg" alt="download time"  width="500" /></a></p>
<p>Please, take with caution and do some testing, don&#8217;t use directly in a production environment.</p>
<p>Of course, I&#8217;ll not give any warranty of success and you have to check the results using the Firebug Extension of Firefox, the &#8220;Live HTTP Headers&#8221; extension and the YSlow plug in of the Firebug Extension.</p>
<p>Let&#8217;s see the three files involved:</p>
<p>First file <strong>.htaccess</strong>:<br />
Modify your .htaccess file to include the following:</p>
<p><strong><br />
AddHandler application/x-httpd-php .css .js<br />
php_value auto_prepend_file gzip-start.php<br />
php_value auto_append_file gzip-end.php<br />
</strong></p>
<p>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.</p>
<p>It&#8217;s a powerful way to create common headers and footers to pages or files, but be aware that it comes at a cost, it&#8217;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&#8217;s worthy to do so.</p>
<p>Second file, the magical <strong>gzip-start.php</strong>:</p>
<p><strong>&lt;?php</strong></p>
<p><strong> function get_http_mdate()<br />
{<br />
return gmdate(&#8217;D, d M Y H:i:s&#8217;, filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF'])).&#8217; GMT&#8217;;<br />
}</strong></p>
<p><strong>function check_modified_header()<br />
{// This function is based on code from http://ontosys.com/php/cache.html<br />
$headers=apache_request_headers();<br />
$if_modified_since=preg_replace(&#8217;/;.*$/&#8217;, &#8221;, $headers['If-Modified-Since']);<br />
if(!$if_modified_since)<br />
{<br />
return;<br />
}</strong></p>
<p><strong>$gmtime=get_http_mdate();</strong></p>
<p><strong>if ($if_modified_since == $gmtime)<br />
{<br />
header(&#8221;HTTP/1.1 304 Not Modified&#8221;);<br />
exit;<br />
}<br />
}</strong></p>
<p><strong>check_modified_header();</strong></p>
<p><strong>// Open a gzipped buffer<br />
ob_start (&#8221;ob_gzhandler&#8221;);</strong></p>
<p><strong>if (strpos($_SERVER['PHP_SELF'], &#8216;.js&#8217;)&gt;0)<br />
{// Javascript file<br />
header(&#8221;Content-type: text/javascript&#8221;);<br />
}<br />
elseif (strpos($_SERVER['PHP_SELF'], &#8216;.css&#8217;)&gt;0)<br />
{// CSS file<br />
header(&#8221;Content-type: text/css&#8221;);<br />
}</strong></p>
<p><strong>$offset = 60000000; // Far far away in time</strong></p>
<p><strong>// Expires<br />
header(&#8221;Expires: &#8220;.gmdate(&#8221;D, d M Y H:i:s&#8221;, time() + $offset) . &#8221; GMT&#8221;);</strong></p>
<p><strong>// Cache-Control<br />
header(&#8221;Cache-Control: must-revalidate, max-age=&#8221;.(time()+$offset));</strong></p>
<p><strong>header(&#8221;Last-Modified: &#8220;.get_http_mdate());</strong></p>
<p><strong>// generate unique ID, using the modification date and the absolute path to the file<br />
$hash = md5(filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']).$_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']);<br />
header(&#8217;Etag: &#8220;&#8216;.$hash.&#8217;&#8221;&#8216;);</strong></p>
<p><strong>?&gt;</strong></p>
<p>The concept is clear, this file is interpreted by the PHP runtime right before the output of the requested file.</p>
<p>Let&#8217;s see the different parts of the file:</p>
<p><strong><br />
check_modified_header();<br />
</strong></p>
<p>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.</p>
<p><strong><br />
// Open a gzipped buffer<br />
ob_start (&#8221;ob_gzhandler&#8221;);</strong></p>
<p><strong>if (strpos($_SERVER['PHP_SELF'], &#8216;.js&#8217;)&gt;0)<br />
{// Javascript file<br />
header(&#8221;Content-type: text/javascript&#8221;);<br />
}<br />
elseif (strpos($_SERVER['PHP_SELF'], &#8216;.css&#8217;)&gt;0)<br />
{// CSS file<br />
header(&#8221;Content-type: text/css&#8221;);<br />
}<br />
</strong></p>
<p>These lines opens a PHP buffer with the trick that it&#8217;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.</p>
<p><strong><br />
$offset = 60000000; // Far far away in time</strong></p>
<p><strong>// Expires<br />
header(&#8221;Expires: &#8220;.gmdate(&#8221;D, d M Y H:i:s&#8221;, time() + $offset) . &#8221; GMT&#8221;);</strong></p>
<p><strong>// Cache-Control<br />
header(&#8221;Cache-Control: must-revalidate, max-age=&#8221;.(time()+$offset));<br />
</strong></p>
<p>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.</p>
<p>Be warned, your CSS and your JS files will be cached in a lot of proxies and client browsers, you have to use a &#8220;rename file policy&#8221; in your HTML to prevent the users from using old versions of the files. We&#8217;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.</p>
<p><strong><br />
header(&#8221;Last-Modified: &#8220;.get_http_mdate());</strong></p>
<p><strong>$hash = md5(filemtime($_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']).$_SERVER['DOCUMENT_ROOT'].$_SERVER['PHP_SELF']);<br />
header(&#8217;Etag: &#8220;&#8216;.$hash.&#8217;&#8221;&#8216;);<br />
</strong></p>
<p>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).</p>
<p>Finally we generate an unique Etag header, based on the update time of the requested file and it&#8217;s full path.</p>
<p>Third and last file, the not less magical <strong>gzip-end.php</strong>:</p>
<p><strong><br />
&lt;?php<br />
header(&#8217;Content-Length: &#8216; . ob_get_length());<br />
ob_end_flush();<br />
?&gt;<br />
</strong></p>
<p>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&#8217;s contents.</p>
<p>Simple uh ?</p>
<p>I hope you enjoyed and finded useful, feel free to change and play with the code.</p>
<p>I would like to thank the sources where I took inspiration and code:</p>
<p>The tutorial of CSS compression,<br />
<a href="http://www.maxkiesler.com/index.php/weblog/comments/facilitate_user_experience_with_css_compression/" target="_blank">Facilitate User Experience with CSS Compression</a>, from Max Kiesler</p>
<p>Good introduction to the problem of speeding up the webpages through caching and zipping javascripts<br />
<a href="http://www.thinkvitamin.com/features/webapps/serving-javascript-fast" target="_blank">Serving Javascript Fast</a></p>
<p>And always a good tutorial even for novices (in Spanish)<br />
<a href="http://www.argo.es/~jcea/artic/web-cache.htm" target="_blank">¿Por qué y cómo crear un espacio web &#8220;cache friendly&#8221;?</a> from jcea</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ipsojobs.com/blog/2007/10/15/cache-and-gzip-your-javascripts-and-csss-files-to-speed-up-your-site/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
	</channel>
</rss>
