Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created March 25, 2014 11:00
Ask Ben: Displaying A Blog Teaser (Showing The First N Words)
<!--- Save some content that might appear in a blog post. --->
<cfsavecontent variable="blogContent">
<p>
Last night, I had the <em>craziest</em> dream about
<strong>Tricia</strong>. It was one of those dreams where
you know you're dreaming beacuse it's <em>way more
awesome</em> than anything that's happened in real life.
</p>
<p>
We're standing in the locker room talking and she asks me
if I would mind if she changed out of her gym clothes
while we're talking... would <em>I mind</em>?!? I say,
"Not at all," trying to mask my wild enthusiasm.
</p>
</cfsavecontent>
<!---
To display the teaser, the first thing I like to do is
strip out all of the formatting tags. Leaving that tags in
just opens yourself up to a world of complexity that you
don't want to deal with.
Let's replace the tags with a space so that we don't cause
any unintended word concatination (this will cause some
extra spacing, but there is only so much fine-tuning we
can do in such a generic process).
--->
<cfset blogContent = reReplace(
blogContent,
"</?\w+(\s*[\w:]+\s*=\s*(""[^""]*""|'[^']*'))*\s*/?>",
" ",
"all"
) />
<!---
Now, let's minimize any white space characters. Technically,
since HTML doesn't render more than one white space character,
this isn't an issue; but, it's something I like to do anyway
for intent.
--->
<cfset blogContent = reReplace(
trim( blogContent ),
"\s+",
" ",
"all"
) />
<!---
Now that we have cleaned our blog content, we need to extract
the first 50 words. While this might not seem like a pattern,
I think using a regular expression for this is a very nice
approach. Think of it this way: the first 50 words is like
finding a pattern that is 50 instances long.
For ease-of-use, we are going to define a word TOKEN as any
non-spaces followed by a space.
NOTE: Because regular expressions are, by default, greedy,
the notation {1,50} will try to match all 50 word tokens
before it tries to match anything less.
--->
<cfset blogSnippets = reMatch(
"([^\s]+\s?){1,50}",
blogContent
) />
<!---
As a safe-guard, let's double-check to make sure that the
reMatch() method returned at least one match. If a blog entry
was purely visual (ie. just an image or set of images), then
no words would be returned. If that is the case, then let's
make up a content description.
--->
<cfif !arrayLen( blogSnippets )>
<!---
Fabricate content snippets. This is an outlier case, but
we want to do this for mathematical stability.
--->
<cfset blogSnippets = [ "Visual content only" ] />
</cfif>
<!---
Since reMatch() returns an array of matches, we have now
broken our entire blog content up into an array in which
each index has 50 or less words. As such, we can use the
first index value as our blog teaser.
But, since we broke up the entire blog post, we can also
now estimate the total number of words in the entire blog
entry (if we want to output it).
We can even go one step further and give the reader an
approciate time investment they would have to make based on
an average number of words per minute.
--->
<cfoutput>
<!---
Get the approximate word count. The first N-1 matches
will be 50; then, we can use the last returned match as
a space-delimitted list.
--->
<cfset wordCount = (
(50 * (arrayLen( blogSnippets ) - 1)) +
listLen( blogSnippets[ arrayLen( blogSnippets ) ], " " )
) />
<!---
Get the estimated time requirement for reading this blog
entry - this assumes that a user can read about 200 words
per minutes. This can be much higher, but for our
purposes, this is fine.
NOTE: Our max here is to make sure the read time never
goes below one minute.
--->
<cfset readingTime = max( round( wordCount / 200 ), 1 ) />
<p>
<!---
The first snippet of 50 words will serve as our
blog teaser
--->
#blogSnippets[ 1 ]#
<span style="white-space: nowrap ;">
<!--- Words and time estimates. --->
[<em>#wordCount# words /
aprx. read time: #readingTime# minute(s)</em>]
&hellip; <a href="##">read more</a> &raquo;
</span>
</p>
</cfoutput>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Displaying A Content Teaser With jQuery</title>
<script type="text/javascript" src="jquery-1.3.2.js"></script>
<script type="text/javascript">
// Let's create a jQuery plugin that creates content
// teasers with word counts and time estimates.
$.fn.teaser = function( teaserOptions ){
// Create the internal options.
var options = $.extend(
{},
$.fn.teaser.options,
teaserOptions
);
// Iterate over each item in this collection such
// that each teaser can be applied individually based
// on targeted information.
this.each(
function(){
var container = $( this );
// First, we want to wrap the contents of the
// container in a new DIV so that we can hide
// it in lieu of our teaser.
var fullContent = container.children()
.wrapAll(
"<div class=\"full-content\"></div>"
)
.parent()
;
// Now, let's get all the text from the
// content container so that we can start
// constructing our teaser.
var rawContent = $.trim( fullContent.text() );
// Remove any extra white space.
rawContent = rawContent.replace(
new RegExp( "\\s+", "g" ),
" "
);
// Break the content up into chunks based on
// the words count.
var snippets = rawContent.match(
new RegExp(
"([^\\s]+\\s?){1," + options.wordCount + "}",
"g"
)
);
// Check to make sure there is at least one
// snippet item.
if (!snippets.length){
snippets = [ "Visual content only" ];
}
// Calculate the word count.
var wordCount = (
(options.wordCount * (snippets.length - 1)) +
snippets[ snippets.length - 1 ].split( " " ).length
);
// Calculate the reading time.
var readingTime = Math.max(
Math.floor( wordCount / options.wordsPerMinute ),
1
);
// Now, let's create the teaser. We will simply
// append it to the original container.
container.append(
"<p class=\"teaser-content\">" +
snippets[ 0 ] +
" [<em>" +
(wordCount + " words / aprx. read time: ") +
(readingTime + " minute(s)") +
"</em>]" +
"... <a href=\"#\">read more</a>" +
"</p>"
);
// Hide the actual content.
fullContent.hide();
}
);
// Now that we have split up the content and created
// the teaser headers, let's hook up the read more
// links to display the content.
this.find( "p.teaser-content a" )
.attr( "href", "javascript:void( 0 )" )
.click(
function(){
var link = $( this );
// Hide the teaser.
link.parent().hide();
// Show the content.
link.parent().prev().show();
// Cancel default event.
return( false );
}
)
;
// Return this collection for method chaining.
return( this );
}
// Define defaults for the content teaser plugin.
// These can be overridden using the options hash when
// calling the teaser method.
$.fn.teaser.options = {
wordCount: 50,
wordsPerMinute: 200
};
// --------------------------------------------------- //
// --------------------------------------------------- //
// When the DOM is ready, initialize.
$(function(){
$( "div.blog-entry" ).teaser();
});
</script>
</head>
<body>
<h1>
Displaying A Content Teaser With jQuery
</h1>
<div class="blog-entry" style="border-bottom: 1px solid black ;">
<p>
Last night, I had the <em>craziest</em> dream about
<strong>Tricia</strong>. It was one of those dreams where
you know you're dreaming beacuse it's <em>way more
awesome</em> than anything that's happened in real life.
</p>
<p>
We're standing in the locker room talking and she asks me
if I would mind if she changed out of her gym clothes
while we're talking... would <em>I mind</em>?!? I say,
"Not at all," trying to mask my wild enthusiasm.
</p>
</div>
<div class="blog-entry" style="border-bottom: 1px solid black ;">
<p>
Last night, I had the <em>craziest</em> dream about
<strong>Tricia</strong>. It was one of those dreams where
you know you're dreaming beacuse it's <em>way more
awesome</em> than anything that's happened in real life.
</p>
<p>
We're standing in the locker room talking and she asks me
if I would mind if she changed out of her gym clothes
while we're talking... would <em>I mind</em>?!? I say,
"Not at all," trying to mask my wild enthusiasm.
</p>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment