2005-11-20

Blogger previous and next date links

It's time for my much sought after post on setting up a Blogger blog with Previous and Next date links. This is one way of doing it, it is not exactly the way my blog does it. (This code is much cleaner, and only does this particular thing. Hence you won't need huge racks of code that probably just break browsers I have not done enough testing with.)

Caveats

Before we begin, note that the present state of the Blogger tags are rather limiting and don't yet make it possible to actually solve this problem all out for post pages, without additional labour on your part (such as manualy maintaining the Previos and Next links in each post's text body -- doable though painful) -- the solution given below gives previous and next date links for your archive pages, not Previous / Next post pages. (They can of course be put on item pages too, but on a busy blogging day with three items posted, both the Previous and Next links will skip over a note or two, jumping straight to the nearest date in either direction.) There, you have been warned, now let's get to it!

Prerequisites

You must make sure your Blogger settings are right, since this method requires you to archive by day. Also, you have to republish the entire blog every time you add a post on a new date, or there will not be any "Next date" links for the days running up to the last post. This is because this solution is based on scanning the Archives secion of your blog for links, comparing the dates between the links available there and the dates of the post visited, to pick out the next and previous dates for the two navigation links. Crude, yet effective. This requires reposting the entire blog, because Blogger would not otherwise add today's archive page link to yesterday's archives section. (And it's either update all posts or just today's. Too bad for us; we'll just have to wait for the extra needless processing of our blog history.) One more thing: by implication, to manage to parse these dates, both your date header and archive date formats must contain all of year, month and date. "Sunday", or "November 20" is not enough.

Anyway, find the Archive settings, and set the Archive Frequency to "Daily":

Settings -> Archive -> Archive frequency: Daily


Then it's time to muck about with the template. Pick the Template tab in the view above, mark all contents, copy them and save them in a file somewhere safe. There is no undo button anywhere in sight, so you want to provide you the option, in case you're not satisfied with the results, or something goes very wrong. (I won't be able to help you, so you had better help yourself. I'm only going to say this once.)

Now seek out the <BlogDateHeader> tag; it should be somewhere in the <body> tag, and you might have to scroll through lots and lots of stylesheets before you find it. Mine looked like this before editing:

<BlogDateHeader>
<div class="DateHeader"><$BlogDateHeaderDate$></div>
</BlogDateHeader>

and we will change it a bit to toss in some additions, marked in bold face below:

<BlogDateHeader>
<div style="float:left; visibility:hidden; padding:0.5em;"
id="prev-date"><a accesskey="P">« Previous Date «</a>
</div>
<div style="float:right; visibility:hidden; padding:0.5em;"
id="next-date"><a accesskey="N">» Next Date »</a>
</div>

<div id="seen-date" class="DateHeader"><$BlogDateHeaderDate$></div>
</BlogDateHeader>

The style attributes and order of these tags isn't important; it's just a sketchy way of making them show up somewhat naturally in the page flow; your own styling should hopefully look a little better, to blend in better with the rest of your blog template. If you so prefer, you may skip the added divs and tuck the id and style attributes on to the <a> tags themselves, instead. The accesskey attributes give nice shortcuts to these links; Alt + P/N in most environments, Shift + Escape followed by P or N in Opera. In other Mac browsers, it's Control + P/N.

This adds the basis for where the navigation will end up. As you see, they will be hidden by default -- the mentioned script code will turn them on, when there is something to be clicked for each link. If you always want both links visible, even when unclickable, delete the "visibility:hidden;" part. Should you want just the key bindings, but not the visible links, substitute with "display:none;" instead.

Next, we must make sure the archive section is adressable by the script. Seek out (or add anew, if there is none) the <BloggerArchives> tag. Mine looked like this:

<BloggerArchives>
<a href="<$BlogArchiveURL$>"><$BlogArchiveName$></a> /
</BloggerArchives>

and after our modifications, it will look like this:

<div id="archive"><BloggerArchives>
<a href="<$BlogArchiveURL$>"><$BlogArchiveName$></a> /
</BloggerArchives></div>

The only bit that matters about it is that there is the <a> tag above somewhere in it and that it is surrounded by a tag with the id="archive" attribute. Below this we will add the big portion of code that does the links. Optionally, you may stow all but the leading makePrevNextLinks() line away somewhere up in your <head> tag, if you prefer. Just make sure that the makePrevNextLinks() line appears in the page flow below the above sections, and things will run just fine.

The final blow is the large chunk of code below. Tune it to use your date format (you may want to open up the Settings / Formatting page in a different browser window to find out what date format settings you use, in case you forgot. The two select boxes below need to use the same format (though they won't tell the same date, unless you are one of the few readers who happen to catch this note today, the same day I wrote it), or the code will not work. When you pick a date format, the first two lines of the code below changes accordingly; should you later feel you want to use some other date format, come back here and readjust them to see what values to put there.

Add the huge <script> block -- if you just want it on the archive pages where it always works reliably, encase it in an <ArchivePage>...</ArchivePage> container, and it won't appear on the main page and item pages. If your choice of Date Language isn't English and either of your date formats includes a month name, also be sure to edit the monthNames variable to read exactly (case matters!) how month names are listed in your language of choice, separated by the vertical bar character. Once you are done, save the template and republish your blog, and voilà -- Previous and Next buttons!

Settings -> Formatting -> Date formats



<script type="text/javascript">
var monthNames = 'January|February|March|April|May|June|July|' +
'August|September|October|November|December';
var dateHeaderParser = getDateHeaderParser( 1 );
var archiveParser = getArchiveParser( 9 );
makePrevNextLinks(); // Don't change anything from here onwards:

function makePrevNextLinks()
{
var prevNode = document.getElementById( 'prev-date' );
var seenNode = document.getElementById( 'seen-date' ), seen;
var nextNode = document.getElementById( 'next-date' );
if( !seenNode ) return alert( 'No tag with id="seen-date" found!' );
if( !(seen = dateHeaderParser( seenNode.innerHTML )) )
return alert( 'Failed to parse date "'+ seenNode.innerHTML +
'"; did you really set the right format?' );
seen = seen.getTime();
var all = getDates(), prev, next;
for( next in all )
if( next == seen )
{
next = null;
continue;
}
else if( next > seen )
break;
else if( next < seen )
{
prev = next;
next = null;
}
if( prev && prevNode )
{
if( prevNode.noveName != 'A' )
prevNode = prevNode.getElementsByTagName( 'a' ).item( 0 );
prevNode.href = all[prev];
prevNode.style.visibility = 'visible';
}
if( next && nextNode )
{
if( nextNode.noveName != 'A' )
nextNode = nextNode.getElementsByTagName( 'a' ).item( 0 );
nextNode.href = all[next];
nextNode.style.visibility = 'visible';
}
}

function getDates()
{
var ar = document.getElementById( 'archive' );
if( ar )
{
var all = ar.getElementsByTagName( 'a' ), dates = {}, i;
for( i=0; i<all.length; i++ )
{
var link = all[i];
var date = archiveParser( link.innerHTML );
if( date ) dates[date.getTime()] = link.href;
}
return dates;
}
}

function getArchiveParser( formatNo )
{
var fmt = { 0:'MonthName Day, Year', 1:'MonthNo/Day/Year',
2:'MonthNo\\.Day\\.Year', 3:'YearMonthNoDay',
4:'Day\\.MonthNo\\.Y2', 5:'Year-MonthNo-Day',
/*6:'MonthName Day',*/ 7:'MonthName Day, Year',
8:'Year/MonthNo/Day', 9:'MonthNo/Day/Y2',
10:'Y2_MonthNo_Day', 11:'Day MonthName Year',
12:'Day MonthName', 13:'Day/MonthNo/Year',
14:'Day/MonthNo/Y2' }[ formatNo ];
return fmt ? getDateParser( fmt ) : alert( 'Archive date format type ' +
(formatNo ? formatNo + ' not supported! (Year needed?)' : ' not given!') );
}

function getDateHeaderParser( formatNo )
{
var fmt = { 1:'MonthName Day, Year', 2:'MonthNo/Day/Year',
3:'MonthNo\\.Day\\.Year', 4:'YearMonthNoDay',
14:'Year/MonthNo/Day', 6:'Year-MonthNo-Day',
5:'Day\\.MonthNo\\.Y2', 7:'MonthNo\\.Day\\.Year',
/*8:'Weekday', 10:'MonthName Day',*/
12:'MonthName Day, Year', 18:'Day MonthName Year',
23:'Day MonthName, Year' }[ formatNo ];

return fmt ? getDateParser( fmt ) : alert( 'Date header format type ' +
(formatNo ? formatNo + ' not supported! (Year needed?)' : ' not given!') );
}

function sortNumeric( a, b )
{
return parseInt( a ) - parseInt( b );
}

function findInArray( array, value )
{
for( var i=0; i<array.length; i++ )
if( array[i] == value ) return i;
return -1;
}

function getDateParser( format )
{
var what = { Year:'\\d{4}', Y2:'\\d{2}', MonthName:monthNames,
MonthNo:'\\d{1,2}', Day:'\\d{1,2}' };
var re = format, monthNo = {}, where = {}, order = [], tmp, i, type;
for( i=0, tmp = what.MonthName.split( '|' ); i<tmp.length; i++ )
monthNo[tmp[i]] = i;
for( type in what ) // for each match type,
if( (tmp = format.indexOf( type )) >= 0 ) // if used in this format,
{
where[tmp] = type; // store away its match position, to find out
order[order.length] = parseInt( tmp ); // which paren matched what
re = re.replace( type, '('+ what[type] +')' ); // and fix the regexp
}
for( i=0, order = order.sort( sortNumeric ); i<order.length; i++ )
order[i] = where[order[i]]; // map back to mnemonics, again
var getYear = function( match )
{
var where = findInArray( order, 'Year' ) + 1;
if( where ) return parseInt( match[where], 10 );
where = findInArray( order, 'Y2' ) + 1;
if( !where ) return (new Date).getFullYear();
var year = parseInt( match[where], 10 ) + 1900;
if( year < 1990 ) year += 100;
return year;
};
var getMonth = function( match )
{
var where = findInArray( order, 'MonthName' ) + 1;
if( where ) return monthNo[ match[where] ];
where = findInArray( order, 'MonthNo' ) + 1;
if( where ) return parseInt( match[where], 10 ) - 1;
return (new Date).getMonth();
};
var getDate = function( match )
{
var where = findInArray( order, 'Day' ) + 1;
if( where ) return parseInt( match[where], 10 );
return (new Date).getDate();
};
re = new RegExp( '\\b'+ re +'\\b', 'i' ); // make it a real RegExp
return function( raw )
{
var match = re.exec( raw );
if( match )
return new Date( getYear(match), getMonth(match), getDate(match) );
};
}
</script>

blog comments powered by Disqus