Implementing
The Boston Globe
Front end architecture and challenges
Front end architecture and challenges
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
Filament Group (@filamentgroup), working with Ethan Marcotte (@beep), delivered static prototypes
3-in-1 javascript framework that provides a "responsive" baseline
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">
A 'controller' script orchestrates qualification and delivery of experience
Also drives Progressive Enhancement, which couples well with RWD
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 );
}
// 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"
}
};
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];
}
}
}
});
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
] );
}
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>
Reliance on media queries means they must work everywhere
https://github.com/filamentgroup/Responsive-Images
<img src="images/pretty-kitty.r.jpg"
data-fullsrc="http://cdn.com/images/pretty-kitty.jpg"
alt="meow"/>
<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>
<div id="video" class="videoplayer" data-schema="1"
data-player="article" data-params="@videoPlayer=1202819473001"> </div>
.videoplayer {
max-width: 100%;
}
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();
};
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
#javascriptthebadparts
Delivering ads on a responsive site creates multiple challenges
@media all and ( min-width: 500px ) {
.ad { display none; }
}
if (!$( '.ad' ).is( ':visible' )) {
$( '.ad' ).appendTo( '.b' );
}
Custom creative built into the site
(not delivered from network)

Dan Middleton @middle2000lb
Jesse Weisbeck @jlweisbeck
A web app that works yesterday, today, and tomorrow.





![]()
85 async mysaved xhr requests

189ms total latency

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 );
}

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 )
}
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);
}
// 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 );
}
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

Caz vonKow @vonkow
Ian Cohen @iancohen