Skip to content
Christian Heilmann
  • Christian Heilmann Avatar
  • About this
  • Archives
  • Slides
  • Bookmarks
  • codepo8
  • @codepo8

You are currently browsing the Christian Heilmann blog archives for January, 2010.

Archive for January, 2010

  • πŸ”™ Older Entries
  • Newer Entries πŸ”œ

TTMMHTM: Bond Mythbusters, security translation, boulevard wordpress and a ten year old tech writer

Monday, January 18th, 2010

Things that made me happy this morning:

  • Frédéric de Villamil took my security article on Smashingmagazine and translated it into French – merci beaucoup!
  • Mythbusters – Bond style is so full of win we need to somehow convince Kari to re-enact it!
  • UK’s largest selling evening newspapers Express and Star are powered by WordPress! Want proof? add /wp-admin to the URI.
  • Mashable has a great tutorial on how to capture youtube videos
  • The world’s most interesting bridges post has some photos of the initial cables for a bridge being shot over to the mountain site with rockets. I wonder who was there to catch them.
  • A ten year old writing tech tutorials – shame that he already drowns in spam comments

Tags: accessibility, captioning, geek, mythbusters, security, ttmmhtm, writing, youtube
Posted in General | 3 Comments »

TTMMHTM: Alt attributes, social media tips, security and Germans vs. IE

Saturday, January 16th, 2010

Things that made me happy this morning:

  • Esteemed colleague Ian Pouncey has a nice little write-up on alt attributes – they are not alt tags, learn it!
  • Three things you need to know about social media strategy has it down to a T.
  • Security in web applications starts like a pretty obvious talk but has some very good examples in it.
  • Should you use JavaScript CDNs is a short review of it really being worth it – it is missing the amount of YUI delivered from yahooapis.com though.
  • Apparently the German government is telling people to stop using IE because of project Aurora (official message in German)
  • Ever wondered what current movie titles would look like as Atari games covers?

  • Afterdawn are discussing the value of digital copies of movies – well, there’s always free tools for conversion, too.

Tags: atari, cdn, digitalcopy, ie, javascript, retro, security
Posted in General | 3 Comments »

Quick hack tutorial – how to build a Google Auto Suggest comparison

Monday, January 11th, 2010

This morning I stumbled upon this funny article on predictably irrational which shows the difference between men and women by using Google Autosuggest:

What boyfriends and girlfriends search for on Google

Google suggest has been the source of many a joke so far (and there are even collections of those available) and I wondered how hard it would be to write a comparison tool.

Turns out it is dead easy, see it here:

Google suggest comparisons - compare the results of two Google suggest searches by  you.

The first step was to find the right API. A Google search for “Google Suggest API” gave me this old blog post on blogoscoped which told me about the endpoint for an XML output of the Google Suggest:

http://google.com/complete/search?output=toolbar&q=chuck+norris

The result is very straight forward XML:





[... repeated 10 times with other data …]

Now to get more than one I could have used two calls, but why do that when you have YQL?

select * from xml where url in (
‘http://google.com/complete/search?output=toolbar&q=chuck+norris’,
‘http://google.com/complete/search?output=toolbar&q=steven+seagal’
)

Using this gives me a dataset with both results:


yahoo:count=”2” yahoo:created=”2010-01-11T03:48:43Z”
yahoo:lang=”en-US” yahoo:updated=”2010-01-11T03:48:43Z”
yahoo:uri=”http://quer[...]al%27%29”>





[... repeated with different data …]





[... repeated with different data …]


Good, that’s the data – now for the interface. Using YUI grids and the grids builder it is easy to build this:


Google suggest comparisons














Written by Chris Heilmann, using YQL and the unofficial Google Autosuggest API.


All that is left is the PHP:


// turn off error reporting and define error as empty
error_reporting(0);
$error = ‘’;

// check if parameters were sent and filter them for display
// and for use in a link.
if(isset($_GET[‘q1’]) && isset($_GET[‘q2’])){
$q1search = filter_input(INPUT_GET, ‘q1’, FILTER_SANITIZE_ENCODED);
$q2search = filter_input(INPUT_GET, ‘q2’, FILTER_SANITIZE_ENCODED);
$q1 = filter_input(INPUT_GET, ‘q1’, FILTER_SANITIZE_SPECIAL_CHARS);
$q2 = filter_input(INPUT_GET, ‘q2’, FILTER_SANITIZE_SPECIAL_CHARS);

// assemble the YQL URI with the q1search and q2search
// as parameters – get back JSON as XML makes my teeth hurt
$url= ‘http://query.yahooapis.com/v1/public/yql?q=’.
‘select%20*%20from%20xml%20where%20url%20in%20’.
‘(‘http%3A%2F%2Fgoogle.com%2Fcomplete%2Fsearch’.
‘%3Foutput%3Dtoolbar%26q%3D’.urlencode($q1search).
‘’%2C’http%3A%2F%2Fgoogle.com%2Fcomplete%2F’.
‘search%3Foutput%3Dtoolbar%26q%3D’.urlencode($q2search).
‘’)&format=json’;

// Use cURL to load it
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);

// decode the data and get the first result set.
$data = json_decode($output);
$res1 = $data->query->results->toplevel[0]->CompleteSuggestion;

// if data came through, assemble the list to be shown
if(sizeof($res1)>0){
$out1 = ‘

    ‘;
    foreach($res1 as $r){
    $out1 .= ‘
  • ‘.$r->suggestion->data.’
  • ‘;
    }

    $out1 .= ‘

‘;
// otherwise assemble an error message
} else {
$out1 = ‘
  • Error: No results found
‘;
}

// do the same for the second set
$res2 = $data->query->results->toplevel[1]->CompleteSuggestion;
if(sizeof($res2)>0){
$out2 = ‘

    ‘;
    foreach($res2 as $r){
    $out2 .= ‘
  • ‘.$r->suggestion->data.’
  • ‘;
    }

    $out2 .= ‘

‘;
} else {
$out = ‘
  • Error: No results found
‘;
}

// if no data was sent, say so…
} else {
$error = ‘Please enter a search term for each box.’;
}

?>

That’s it! Adding a lick of CSS and we have the final product.

Posted in General | 3 Comments »

Loading external content with Ajax using jQuery and YQL

Sunday, January 10th, 2010

Let’s solve the problem of loading external content (on other domains) with Ajax in jQuery. All the code you see here is available on GitHub and can be seen on this demo page so no need to copy and paste!

OK, Ajax with jQuery is very easy to do – like most solutions it is a few lines:

$(document).ready(function(){
  $('.ajaxtrigger').click(function(){
    $('#target').load('ajaxcontent.html');
  });
});

$(document).ready(function(){ $('.ajaxtrigger').click(function(){ $('#target').load('ajaxcontent.html'); }); });

Check out this simple and obtrusive Ajax demo to see what it does.

This will turn all elements with the class of ajaxtrigger into triggers to load “ajaxcontent.html” and display its contents in the element with the ID target.

This is terrible, as it most of the time means that people will use pointless links like “click me” with # as the href, but this is not the problem for today. I am working on a larger article with all the goodies about Ajax usability and accessibility.

However, to make this more re-usable we could do the following:

$(document).ready(function(){
  $('.ajaxtrigger').click(function(){
    $('#target').load($(this).attr('href'));
    return false;
  });
});

$(document).ready(function(){ $('.ajaxtrigger').click(function(){ $('#target').load($(this).attr('href')); return false; }); });

You can then use load some content to load the content and you make the whole thing re-usable.

Check out this more reusable Ajax demo to see what it does.

The issue I wanted to find a nice solution for is the one that happens when you click on the second link in the demo: loading external files fails as Ajax doesn’t allow for cross-domain loading of content. This means that see my portfolio will fail to load the Ajax content and fail silently at that. You can click the link until you are blue in the face but nothing happens. A dirty hack to avoid this is just allowing the browser to load the document if somebody really tries to load an external link.

Check out this allowing external links to be followed to see what it does.

$(document).ready(function(){
  $('.ajaxtrigger').click(function(){
    var url = $(this).attr('href');
    if(url.match('^http')){
      return true;
    } else {
      $('#target').load(url);
      return false;
    }
  });
});

$(document).ready(function(){ $('.ajaxtrigger').click(function(){ var url = $(this).attr('href'); if(url.match('^http')){ return true; } else { $('#target').load(url); return false; } }); });

Proxying with PHP

If you look around the web you will find the solution in most of the cases to be PHP proxy scripts (or any other language). Something using cURL could be for example proxy.php:

<?php
$url = $_GET['url'];
$ch = curl_init(); 
curl_setopt($ch, CURLOPT_URL, $url); 
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
$output = curl_exec($ch); 
curl_close($ch);
echo $content;
?>

<?php $url = $_GET['url']; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); curl_close($ch); echo $content; ?>

People then could use this with a slightly changed script (using a proxy):

$(document).ready(function(){
  $('.ajaxtrigger').click(function(){
    var url = $(this).attr('href');
    if(url.match('^http')){
      url = 'proxy.php?url=' + url;
    }
    $('#target').load(url);
    return false;
  });
});

$(document).ready(function(){ $('.ajaxtrigger').click(function(){ var url = $(this).attr('href'); if(url.match('^http')){ url = 'proxy.php?url=' + url; } $('#target').load(url); return false; }); });

It is also a spectacularly stupid idea to have a proxy script like that. The reason is that without filtering people can use this to load any document of your server and display it in the page (simply use firebug to rename the link to show anything on your server), they can use it to inject a mass-mailer script into your document or simply use this to redirect to any other web resource and make it look like your server was the one that sent it. It is spammer’s heaven.

Use a white-listing and filtering proxy!

So if you want to use a proxy, make sure to white-list the allowed URIs. Furthermore it is a good plan to get rid of everything but the body of the other HTML document. Another good idea is to filter out scripts. This prevents display glitches and scripts you don’t want executed on your site to get executed.

Something like this:

<?php
$url = $_GET['url'];
$allowedurls = array(
  'http://developer.yahoo.com',
  'http://icant.co.uk'
);
if(in_array($url,$allowedurls)){
  $ch = curl_init(); 
  curl_setopt($ch, CURLOPT_URL, $url); 
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
  $output = curl_exec($ch); 
  curl_close($ch);
  $content = preg_replace('/.*<body[^>]*>/msi','',$output);
  $content = preg_replace('/<\/body>.*/msi','',$content);
  $content = preg_replace('/<?\/body[^>]*>/msi','',$content);
  $content = preg_replace('/[\r|\n]+/msi','',$content);
  $content = preg_replace('/<--[\S\s]*?-->/msi','',$content);
  $content = preg_replace('/<noscript[^>]*>[\S\s]*?'.
                          '<\/noscript>/msi',
                          '',$content);
  $content = preg_replace('/<script[^>]*>[\S\s]*?<\/script>/msi',
                          '',$content);
  $content = preg_replace('/<script.*\/>/msi','',$content);
  echo $content;
} else {
  echo 'Error: URL not allowed to load here.';
}
?>

<?php $url = $_GET['url']; $allowedurls = array( 'http://developer.yahoo.com', 'http://icant.co.uk' ); if(in_array($url,$allowedurls)){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); curl_close($ch); $content = preg_replace('/.*<body[^>]*>/msi','',$output); $content = preg_replace('/<\/body>.*/msi','',$content); $content = preg_replace('/<?\/body[^>]*>/msi','',$content); $content = preg_replace('/[\r|\n]+/msi','',$content); $content = preg_replace('/<--[\S\s]*?-->/msi','',$content); $content = preg_replace('/<noscript[^>]*>[\S\s]*?'. '<\/noscript>/msi', '',$content); $content = preg_replace('/<script[^>]*>[\S\s]*?<\/script>/msi', '',$content); $content = preg_replace('/<script.*\/>/msi','',$content); echo $content; } else { echo 'Error: URL not allowed to load here.'; } ?>

Pure JavaScript solution using YQL

But what if you have no server access or you want to stay in JavaScript? Not to worry – it can be done. YQL allows you to load any HTML document and get it back in JSON. As jQuery has a nice interface to load JSON, this can be used together to achieve what we want to.

Getting HTML from YQL is as easy as using:

select * from html where url="http://icant.co.uk"

select * from html where url="http://icant.co.uk"

YQL does a few things extra for us:

  • It loads the HTML document and sanitizes it
  • It runs the HTML document through HTML Tidy to remove things .NETnasty frameworks considered markup.
  • It caches the HTML for a while
  • It only returns the body content of the HTML - so no styling (other than inline styles) will get through.

As output formats you can choose XML or JSON. If you define a callback parameter for JSON you get JSON-P with all the HTML as a JavaScript Object – not fun to re-assemble:

foo({
  "query":{
  "count":"1",
  "created":"2010-01-10T07:51:43Z",
  "lang":"en-US",
  "updated":"2010-01-10T07:51:43Z",
  "uri":"http://query.yahoo[...whatever...]k%22",
  "results":{
    "body":{
      "div":{
        "id":"doc2",
        "div":[{"id":"hd",
          "h1":"icant.co.uk - everything Christian Heilmann"
        },
        {"id":"bd",
        "div":[
        {"div":[{"h2":"About this and me","
        [... and so on...]
}}}}}}}});

foo({ "query":{ "count":"1", "created":"2010-01-10T07:51:43Z", "lang":"en-US", "updated":"2010-01-10T07:51:43Z", "uri":"http://query.yahoo[...whatever...]k%22", "results":{ "body":{ "div":{ "id":"doc2", "div":[{"id":"hd", "h1":"icant.co.uk - everything Christian Heilmann" }, {"id":"bd", "div":[ {"div":[{"h2":"About this and me"," [... and so on...] }}}}}}}});

When you define a callback with the XML output you get a function call with the HTML data as string in an Array – much easier:

foo({
  "query":{
  "count":"1",
  "created":"2010-01-10T07:47:40Z",
  "lang":"en-US",
  "updated":"2010-01-10T07:47:40Z",
  "uri":"http://query.y[...who cares...]%22"},
  "results":[
    "<body>\n    <div id=\"doc2\">\n<div id=\"hd\">\n 
     <h1>icant.co.uk - \n
     everything Christian Heilmann<\/h1>\n 
      ... and so on ..."
  ]
});

foo({ "query":{ "count":"1", "created":"2010-01-10T07:47:40Z", "lang":"en-US", "updated":"2010-01-10T07:47:40Z", "uri":"http://query.y[...who cares...]%22"}, "results":[ "<body>\n <div id=\"doc2\">\n<div id=\"hd\">\n <h1>icant.co.uk - \n everything Christian Heilmann<\/h1>\n ... and so on ..." ] });

Using jQuery’s getJSON() method and accessing the YQL endpoint this is easy to implement:

$.getJSON("http://query.yahooapis.com/v1/public/yql?"+
          "q=select%20*%20from%20html%20where%20url%3D%22"+
          encodeURIComponent(url)+
          "%22&format=xml'&callback=?",
  function(data){
    if(data.results[0]){
      var data = filterData(data.results[0]);
      container.html(data);
    } else {
      var errormsg = '<p>Error: can't load the page.</p>';
      container.html(errormsg);
    }
  }
);

$.getJSON("http://query.yahooapis.com/v1/public/yql?"+ "q=select%20*%20from%20html%20where%20url%3D%22"+ encodeURIComponent(url)+ "%22&format=xml'&callback=?", function(data){ if(data.results[0]){ var data = filterData(data.results[0]); container.html(data); } else { var errormsg = '<p>Error: can't load the page.</p>'; container.html(errormsg); } } );

Putting it all together you have a cross-domain Ajax solution with jQuery and YQL:

$(document).ready(function(){
  var container = $('#target');
  $('.ajaxtrigger').click(function(){
    doAjax($(this).attr('href'));
    return false;
  });
  function doAjax(url){
    // if it is an external URI
    if(url.match('^http')){
      // call YQL
      $.getJSON("http://query.yahooapis.com/v1/public/yql?"+
                "q=select%20*%20from%20html%20where%20url%3D%22"+
                encodeURIComponent(url)+
                "%22&format=xml'&callback=?",
        // this function gets the data from the successful 
        // JSON-P call
        function(data){
          // if there is data, filter it and render it out
          if(data.results[0]){
            var data = filterData(data.results[0]);
            container.html(data);
          // otherwise tell the world that something went wrong
          } else {
            var errormsg = '<p>Error: can't load the page.</p>';
            container.html(errormsg);
          }
        }
      );
    // if it is not an external URI, use Ajax load()
    } else {
      $('#target').load(url);
    }
  }
  // filter out some nasties
  function filterData(data){
    data = data.replace(/<?\/body[^>]*>/g,'');
    data = data.replace(/[\r|\n]+/g,'');
    data = data.replace(/<--[\S\s]*?-->/g,'');
    data = data.replace(/<noscript[^>]*>[\S\s]*?<\/noscript>/g,'');
    data = data.replace(/<script[^>]*>[\S\s]*?<\/script>/g,'');
    data = data.replace(/<script.*\/>/,'');
    return data;
  }
});

$(document).ready(function(){ var container = $('#target'); $('.ajaxtrigger').click(function(){ doAjax($(this).attr('href')); return false; }); function doAjax(url){ // if it is an external URI if(url.match('^http')){ // call YQL $.getJSON("http://query.yahooapis.com/v1/public/yql?"+ "q=select%20*%20from%20html%20where%20url%3D%22"+ encodeURIComponent(url)+ "%22&format=xml'&callback=?", // this function gets the data from the successful // JSON-P call function(data){ // if there is data, filter it and render it out if(data.results[0]){ var data = filterData(data.results[0]); container.html(data); // otherwise tell the world that something went wrong } else { var errormsg = '<p>Error: can't load the page.</p>'; container.html(errormsg); } } ); // if it is not an external URI, use Ajax load() } else { $('#target').load(url); } } // filter out some nasties function filterData(data){ data = data.replace(/<?\/body[^>]*>/g,''); data = data.replace(/[\r|\n]+/g,''); data = data.replace(/<--[\S\s]*?-->/g,''); data = data.replace(/<noscript[^>]*>[\S\s]*?<\/noscript>/g,''); data = data.replace(/<script[^>]*>[\S\s]*?<\/script>/g,''); data = data.replace(/<script.*\/>/,''); return data; } });

This is rough and ready of course. A real Ajax solution should also consider timeout and not found scenarios. Check out the full version with loading indicators, error handling and yellow fade for inspiration.

Tags: ajax, crossdomain, javascript, jquery, php, proxy, yql
Posted in General | 29 Comments »

Cleaning up the “CSS only sexy bookmark” demo code

Friday, January 8th, 2010

Going through my Google Reader I stumbled upon an article today called Sexy bookmark like effect in pure CSS. Normally when I hear “pure CSS” I skip as 99% of these solutions don’t work with a keyboard and are thus a bad idea to use on the web. However, this one intrigued me as I had no clue what a “sexy bookmark like effect” might be.

Turns out it was not a porn bookmark but one of those “share this with the social media” link-bars you have below blog posts to help the copy and paste challenged people out there:

Link menu with different social media options

OK, that can’t be that problematic. The trick to do a list of links as a cool rollover using CSS only has been done in 2004 in the first CSS sprites article. Notice that the links are in a list.

Now, the new article’s HTML is the following:








There are a few things wrong with this in my book:

  • There is no semantic structure of what is going on here. Line breaks do not mean anything to HTML so in essence this is a list of links without any separation. Imagine sending three links to a friend in an email or putting them on a page. Would you do it like this: GoogleI can has cheezburgerb3taSpotify ? looks confusing to me…
  • There is no content in the links – when CSS is turned off you find nothing whatsoever.
  • There is quite a repetion of classes there. When every element in another element has the same class in it then something fishy is going on. Unless this class is used as a handle for – let’s say a microformat, you can get rid of it and use the cascade in CSS. So in this case you can style all the links with .sharing-cl a{} and get rid of the repeated classes.
  • A navigation is a structured thing, so instead of a div with links in it, how about using a list? This way when CSS is off, this still makes sense.

So here’s my replacement:


  • email

  • feed

  • twitter

  • facebook

  • stumbleupon

  • digg

Of course you should replace the empty href attributes with the real links.

Normally I’d use IDs instead of classes, but as this bar might be used several times in the document, let’s leave it like it is.

The HTML now is 318 bytes instead of 294 which is a slight increase. But:

  • It makes sense without CSS
  • It is well-structured and makes sense even to screen readers
  • The links make sense as they say where they are pointing to.

Let’s check on the CSS:

.sharing-cl{
}

.sharing-cl a{
display:block;
width:75px;
height:30px;
float:left;
}

.sharing-cl .share-sprite{
background:url(http://webdeveloperjuice.com/demos/images/share-sprite.png) no-repeat}
.sharing-cl .sh-su{
margin-right:5px;
background-position:-210px -40px;
}

.sharing-cl .sh-feed{
margin-right:5px;
background-position:-70px -40px;
}

.sharing-cl .sh-tweet{
margin-right:5px;
background-position:-140px -40px;
}

.sharing-cl .sh-mail{
margin-right:5px;
background-position:0 -40px;
}

.sharing-cl .sh-digg{
margin-right:5px;
background-position:-280px -40px;
}

.sharing-cl .sh-face{
background-position:-350px -40px;
}

.sharing-cl .sh-mail:hover{
margin-right:5px;
background-position:0 1px;
}

.sharing-cl .sh-feed:hover{
margin-right:5px;
background-position:-70px 1px;
}

.sharing-cl .sh-tweet:hover{
margin-right:5px;
background-position:-140px 1px;
}

.sharing-cl .sh-su:hover{
margin-right:5px;
background-position:-210px 1px;
}

.sharing-cl .sh-digg:hover{
margin-right:5px;
background-position:-280px 1px;
}

.sharing-cl .sh-face:hover{
background-position:-350px 1px;
}

So here we have a lot of repetition. You also see where the share-sprite class comes in: if you wanted to add an element to that section that is a link but has no image background you just leave out the class. This, however is exactly the wrong approach to CSS. We can assume that every link in this construct gets the background image, which is why it makes more sense to apply the image to the a element with .sharing-cl a{}. As every link has a class you can easily override this as the “odd one out” with for example .sharing-cl a.plain{}.

The same applies to the margin-right:5px. If that is applied to all the links but one, don’t define it for all the others and leave it out at the “odd one out”. Instead, only apply it to the odd one out and save a lot of code.

Final CSS:

.sharing-cl{
overflow:hidden;
margin:0;
padding:0;
list-style:none;
}

.sharing-cl a{
overflow:hidden;
width:75px;
height:30px;
float:left;
margin-right:5px;
text-indent:-300px;
}

.sharing-cl a{
background:url(http://webdeveloperjuice.com/demos/images/share-sprite.png) no-repeat;
}

a.sh-su{background-position:-210px -40px;}
a.sh-feed{background-position:-70px -40px;}
a.sh-tweet{background-position:-140px -40px;}
a.sh-mail{background-position:0 -40px;}
a.sh-digg{background-position:-280px -40px;}
a.sh-face{
margin-right:0;
background-position:-350px -40px;
}

a.sh-mail:hover{background-position:0 1px;}
a.sh-feed:hover{background-position:-70px 1px;}
a.sh-tweet:hover{background-position:-140px 1px;}
a.sh-su:hover{background-position:-210px 1px;}
.sh-digg:hover{background-position:-280px 1px;}
a.sh-face:hover{
margin-right:0;
background-position:-350px 1px;
}

From 1028 bytes down to 880. Just by understanding how CSS works and how the cascade can be used to your advantage. I would have loved to get rid of the a selectors, too, but they are needed for specificity. Notice the overflow on the main selector – this fixes the issue of the floats not being cleared in the original CSS. By using negative text-indent we get rid of the text being displayed, too. Personally I think this is bad and you should try to show the text as you cannot expect end users to know all these icons.

For example:

#text{
margin-top:3em;
font-weight:bold;
font-family:helvetica,arial,sans-serif;
}

#text a{
text-indent:0;
height:auto;
text-align:center;
font-size:11px;
padding-top:35px;
color:#999;
text-decoration:none;
}

You can see the solution in action here:

Sharing bar - cleaned up by  you.

To me, praising “CSS only solutions” is not enough – if you really love CSS and see it as a better solution than JavaScript then you should also show how people can use its features to create smart, short and flexible code.

Tags: accessibility, bestpractice, cascade, code, css, tutorial, usability
Posted in General | 13 Comments »

  • < Older Entries
  • Newer Entries >
Skip to search
Christian Heilmann is the blog of Christian Heilmann chris@christianheilmann.com (Please do not contact me about guest posts, I don't do those!) a Principal Program Manager living and working in Berlin, Germany.

Theme by Chris Heilmann. SVG Icons by Dan Klammer . Hosted by MediaTemple. Powered by Coffee and Spotify Radio.

Get the feed, all the cool kids use RSS!