Implementing
The Boston Globe

Front end architecture and challenges

http://bo.st/thfTx3

What are we talking about?

  1. Javascript architecture
  2. A few challenges building the site as it is now
  3. Future considerations

And of course Responsive web design! (But within the context of The Boston Globe.)

What about media query hotness? Get "Responsive Web Design" by Ethan Marcotte

Brief history of the front end

Filament Group (@filamentgroup), working with Ethan Marcotte (@beep), delivered static prototypes

3-in-1 javascript framework that provides a "responsive" baseline

Basic experience by default

Default HEAD

<link href="_css/globe-basic.css" rel="stylesheet" id="basic-css">

<script src="_js/lib/rwd-images.js,lib/respond.min.js,
lib/modernizr.custom.min.js,globe-define.js, globe-controller.js">
		

Globe framework

 

A 'controller' script orchestrates qualification and delivery of experience

Also drives Progressive Enhancement, which couples well with RWD

Let's see some code! →

globe-controller.js: browser capability

Capabilities testing to determine if your browser is good, or IE.

globe = {};
// 'Enhanced' means the browser is capable of rendering better features 
globe.enhanced 	= respond.mediaQueriesSupported || globe.browser.ie6
|| globe.browser.ie7 || globe.browser.ie8;

//non-mq-supporting browsers, exit here
if( !globe.enhanced ){ 
	return;
}
//remove the basic stylesheet
var basicCSS = doc.getElementById( "basic-css" );
if( basicCSS ){
	head.removeChild( basicCSS );
}

globe-controller.js: Asset inventory

// Here we inventory all our assets so we can conditionally load them later ...
globe.assets = {
	js: {
		// libraries
		jQuery		: "lib/jquery.js",
		uiCore		: "lib/jquery-ui-core.min.js",
		touch		: "lib/jquery.touch.js",
		(...)

		//globe-specific
		common		: "globe-common.js",
		masthead	: "globe-masthead.js",
		(...)
	},
	css: {
		fonts 		: "globe-fonts.css",
		savedDrawer	: "globe-saved-drawer.css"
	}
};

globe-controller.js: Get a context

Use body classes to provide page context

bodyready is a custom DOMready routine that waits for document.body before executing

globe.bodyready(function(){	
var body	 = doc.body,
  tmplTypes	= [ ... ],
  section	= [ ...];
  lLength = tmplTypes.length > sections.length?tmplTypes.length:sections.length;
  for(var x=0; x < lLength; x++){
	if(tmplTypes[x]){
		if(globe.hasClass( body, "type-"+tmplTypes[x])){
			globe.tmplType = tmplTypes[x];
		}
	}
	if(sections[x]){
		if(globe.hasClass( body, "section-"+sections[x])){
			globe.section = sections[x];
		}
	}
  }
});	

globe-controller.js: Qualified loading

Package up contextualized css/js assets

if( window.screen.width > 480 && !globe.dev.mobileOverride ){
	cssToLoad.push( globe.assets.css.fonts );
}
else{
	docElem.className += " non-fontface";
}

if( globe.support.touch && !savedApp ) {
	jsToLoad.push(globe.assets.js.touch);
}
//photo galleries
if( gallery ){
	jsToLoad = jsToLoad.concat( [
		globe.assets.js.scrollTo,
		globe.assets.js.gallery
	] );
}

globe-controller.js: Writing to the DOM

The script and style methods write the package of assets to the HEAD

globe.load.script( globe.config.path.js + jsToLoad.join(",") );
globe.load.style( globe.config.path.css + cssToLoad.join(",") );

This is what the final markup for javascripts looks like

<script src="/js/lib/jquery.js,lib/jquery.throttledresize.js,
lib/jquery.carousel.js,lib/jquery.collapsible.js,lib/jquery.stickyscroll.js">
</script>
			
		

Media queries

 

Reliance on media queries means they must work everywhere

Ahem ... < IE9

Respond.js polyfill

Responsive Images

Responsive Images

https://github.com/filamentgroup/Responsive-Images

Responsive Images

<img src="images/pretty-kitty.r.jpg" 
data-fullsrc="http://cdn.com/images/pretty-kitty.jpg"
alt="meow"/>

Responsive Images

<rule last="true">
  <from>.*\.r\.(jpe?g|JPE?G|png|gif)$</from>
  <forward>/images/rwd.gif</forward>
  <condition type="cookie" name="rwdimgsize">large</condition>
</rule>
	    
<rule>
  <from>(/rf/image_r/.*)\.r(\.(jpe?g|JPE?G|png|gif))$</from>
  <forward>$1$2</forward>
</rule>
	  

Responsive Images

Responsive video

Responsive video

Example:

<div id="video" class="videoplayer" data-schema="1" 
data-player="article" data-params="@videoPlayer=1202819473001"> </div>

Responsive video

.videoplayer {
  max-width: 100%;
}

Responsive video

var schemas = [
      {
        _default: {
          build_mode: 'brightcove',
          _init: init_brightcove,
          className: 'BrightcoveExperience',
          params: {
            wmode: 'transparent',
            bgcolor: '#FFFFFF',
            publisherID: '245991542',
            isVid: 'true',
            isUI: 'true',
            dynamicStreaming: 'true'
          }
        },
        article: {
          params: {
            width: totalWidth,
            height: totalHeight,
            playerID: '876399703001'
          }
        }
     ];
// Create object.
    var id = 'myExperience' + ++brightcove_player_id,
      obj = $('<object/>')
        .attr( 'id', id )
        .addClass( options.className )
        .get(0);
    
// Create params.
    $.each( options.params, function( name, value ) {
      var param = $('<param/>')
        .attr( 'name', name.toString().replace( /"/g, '"' ) )
        .attr( 'value', value.toString().replace( /"/g, '"' ) )
        .get(0);
      obj.append( param );
    });

// Append it!
    this.html( obj );
    
// Initialize BrightCove.
    BCHTML5.id = id;
    BCHTML5.init({
      token: BCReadAPIToken,
      id: id
    });

    brightcove.createExperiences();
  };

Future Considerations

Managing growth

Better asset management

 

Want more modular control so we can scale sanely

How about module dependency management (AMD)?

Need to maintain a lightweight test suite and site-wide defaults

Ad networks and archaic practices

#javascriptthebadparts

Delivering ads on a responsive site creates multiple challenges

Our ad solution

@media all and ( min-width: 500px ) {
  .ad { display none; }
}

if (!$( '.ad' ).is( ':visible' )) {
  $( '.ad' ).appendTo( '.b' );
}

Dealing with ad networks

Responsive ads? We made one

Custom creative built into the site
(not delivered from network)

Dan Middleton @middle2000lb

Jesse Weisbeck @jlweisbeck

Thanks!

My Saved

A web app that works yesterday, today, and tomorrow.

Why??!

Implementing My Saved

Mysaved Title

The Homepage

Homepage Mysaved Stats

Is this saved?

Mysaved Xhr 200 204

Controlling Mysaved javascript

savedDrawer = window.screen.width > 480 
          && !globe.support.touch 
          && !globe.browser.ie6 
          && !globe.dev.mobileOverride
          && !globe.hasClass( body, "no-saved-drawer");
	
if ( loggedIn ) {
  globe.saved = {
    drawer          : savedDrawer,
    saveArticleUrl  : "/saved/article",
    savedContentUrl : "/_ajax/saved/content.jpt",
    savedPreviewUrl : "/_ajax/saved/preview.jpt"
  };
  
  ...
  
}
if ( globe.support.localStorage ){
    jsToLoad.push( globe.assets.js.json2 )
    jsToLoad.push( globe.assets.js.savedStorage )
}
jsToLoad.push ( globe.assets.js.saved );

if( savedApp ){
    jsToLoad.push( globe.assets.js.savedApp );
}
if( savedDrawer && !savedApp ){
    jsToLoad = jsToLoad.concat( [
        globe.assets.js.uiCore,
        globe.assets.js.uiWidget
            ...
        globe.assets.js.savedDrawer
	] );

    cssToLoad.push( globe.assets.css.savedDrawer );
}

Local Storage

Mysaved Local Storage

syncing

function updateSaved( callback ){
    var request = $.get( saveArticlesUrl, 
                        {r : Math.random().toString().substr(2,6)} ),
        success = function(data, status, jxhr){
            globe.saved.items = data.items
            globe.saved.counts = data.counts
            if (globe.saved.storage) globe.saved.storage.update()
            if (callback) callback()
        }
        error = function(data, status, jxhr){
            if (globe.saved.storage) globe.saved.storage.update()
            if (callback) callback()
        }
    request
        .success( success )
        .error( error )
}
		

... more syncing

function update(){
    var tmp         = {},
        storedItems = getItems(),
        newItems    = globe.saved.items;
    if (newItems === undefined) {
        globe.saved.items = storedItems;
        globe.saved.counts = getCounts();
        return;
    } 
    else {
        setCounts( globe.saved.counts )
    }
    meta = getMeta();
    if (meta.version != version) {
        s.clear();
        meta.version = version;
        setMeta(meta);
    }

updating local storage index

    // Existing items set to -1
    $.each( storedItems, function( i, item ){
        tmp[item.uuid] = -1;
    })
    // New items set to 0. All items +1'd
    $.each( newItems, function (i, item){
        if (!tmp.hasOwnProperty(item.uuid)){
            tmp[item.uuid] = 0;
        }
        tmp[item.uuid] += 1; 
    })
    // Delete -1's, Add +1's, Leave 0's alone
    $.each( tmp, function (uuid, state){
        if (state == -1){
            delPreview(uuid);
            delContent(uuid);
        }
        else if (state == 1){
            setPreview(uuid);
            setContent(uuid);
        }
    })
    setItems( newItems );
}
		

accessing local data

function getPreview(id){
    return s.getItem(previewPrefix+id)
}

function setPreview(id, preview){
    if (preview || (preview = getPreview(id))) {
        s.setItem(previewPrefix+id, preview)
    }
    else {
        $.get( globe.saved.savedPreviewUrl+"?uuid="+id, 
            function(preview) {
                s.setItem(previewPrefix+id, preview);
        })          
    }
}
		
function updateArticles(){
    var gss           = globe.saved.storage,
        articleUl     = $( ".saved-articles>ul"),
        activeSection = $( "ul.article-list" ).data("section"),
        itemCount     = globe.saved.items.length;
    articleUl.empty();

    $.each( globe.saved.items, function( i, item ){
        var preview; 
        if ( gss && ( preview = gss.getPreview( item.uuid ) ) ) {
            appendPreviewLi( item, preview, articleUl, activeSection);
        }
        else {
            $.ajax({url: globe.saved.savedPreviewUrl,
                data: { uuid: item.uuid },
                async: false,
                success: function( preview ){
                    if ( gss ) gss.setPreview( item.uuid, preview );
                    appendPreviewLi( item, preview, articleUl, activeSection)
                }
            })
        }
    });

}
		

Only fetch saved article content once

Application Cache

Mysaved App Cache

Caz vonKow @vonkow

Ian Cohen @iancohen

Thanks!