The last few chapters dealt with getting data into and out of CouchDB. You learned how to model your data into documents and retrieve it via the HTTP API. In this chapter, we’ll look at the views used to power Sofa’s index page, and the list function that renders those views as HTML or XML, depending on the client’s request.
Now that we’ve successfully created a blog post and rendered it as HTML, we’ll be building the front page where visitors will land when they’ve found your blog. This page will have a list of the 10 most recent blog posts, with titles and short summaries. The first step is to write the MapReduce query that constructs the index used by CouchDB at query time to find blog posts based on when they were written.
In Chapter 6, we noted that reduce isn’t needed for many common queries. For the index page, we’re only interested in an ordering of the posts by date, so we don’t need to use a reduce function, as the map function alone is enough to order the posts by date.
You’re now ready to write the map function that builds a list of all blog posts. The goals for this view are simple: sort all blog posts by date.
Here is the source code for the view function. I’ll call out the important bits as we encounter them.
function
(
doc
)
{
if
(
doc
.
type
==
"post"
)
{
The first thing we do is ensure that the document we’re dealing with
is a post. We don’t want comments or anything other than blog posts
getting on the front page. The expression doc.type ==
"post"
evaluates to true for posts but no other kind of
document. In Chapter 7, we saw that the
validation function gives us certain guarantees about posts, designed to
make us comfortable about putting them on the front page of our
blog.
var
summary
=
(
doc
.
html
.
replace
(
/<(.|\n)*?>/g
,
''
).
substring
(
0
,
350
)
+
'...'
);
This line shortens the blog post’s HTML (generated from Markdown before saving) and strips out most tags and images, at least well enough to keep them from showing up on the index page, for brevity.
The next section is the crux of the view. We’re emitting for each
document a key (doc.created_at
) and a value. The key is
used for sorting, so that we can pull out all the posts in a particular
date range efficiently.
emit
(
doc
.
created_at
,
{
html
:
doc
.
html
,
summary
:
summary
,
title
:
doc
.
title
,
author
:
doc
.
author
});
The value we’ve emitted is a JavaScript object, which copies some fields from the document (but not all), and the summary string we’ve just generated. It’s preferable to avoid emitting entire documents. As a general rule, you want to keep your views as lean as possible. Only emit data you plan to use in your application. In this case we emit the summary (for the index page), the HTML (for the Atom feed), the blog post title, and its author.
}
};
You should be able to follow the definition of the previous map
function just fine by now. The emit()
call creates an
entry for each blog post document in our view’s result set. We’ll call the
view recent-posts
. Our design document looks like this
now:
{
"_design/sofa"
,
"views"
:
{
"recent-posts"
:
{
"map"
:
"function(doc) { if (doc.type == "
post
") { ... code to emit posts ... }"
}
}
"_attachments"
:
{
...
}
}
CouchApp manages aggregating the filesystem files into our JSON design document, so we can edit our view in a file called views/recent-posts/map.js. Once the map function is stored on the design document, our view is ready to be queried for the latest 10 posts. Again, this looks very similar to displaying a single post. The only real difference now is that we get back an array of JSON objects instead of just a single JSON object.
The GET request to the URI is:
/blog/_design/sofa/_view/recent-posts
A view defined in the document
/database/_design/designdocname in the
views
field ends up being callable under
/database/_design/designdocname/_view/viewname.
You can pass in HTTP query arguments to customize your view query. In this case, we pass in:
descending: true, limit: 5
This gets the latest post first and only the first five posts in all.
The actual view request URL is:
/blog/_design/sofa/_view/recent-posts?descending=true&limit=5
The _list
function was covered in detail in Chapter 5. In our example application, we’ll use a
JavaScript list function to render a view of recent blog posts as both XML
and HTML formats. CouchDB’s JavaScript view server also ships with the
ability to respond appropriately to HTTP content negotiation and Accept
headers.
The essence of the _list
API is a function that
is fed one row at a time and sends the response back one chunk at a
time.
Let’s take a look at Sofa’s list function. This is a rather long listing, and it introduces a few new concepts, so we’ll take it slow and be sure to cover everything of interest.
function
(
head
,
req
)
{
// !json templates.index
// !json blog
// !code vendor/couchapp/path.js
// !code vendor/couchapp/date.js
// !code vendor/couchapp/template.js
// !code lib/atom.js
The top of the function declares the arguments
head
and req
. Our function does
not use head
, just req
, which
contains information about the request such as the headers sent by the
client and a representation of the query string as sent by the client.
The first lines of the function are CouchApp macros that pull in code
and data from elsewhere in the design document. As we’ve described in
more detail in Chapter 11, these macros
allow us to work with short, readable functions that pull in library
code from elsewhere in the design document. Our list function uses the
CouchApp JavaScript helpers for generating URLs
(path.js), for working with date objects
(date.js), and the template function we’re using to
render HTML.
var
indexPath
=
listPath
(
'index'
,
'recent-posts'
,{
descending
:
true
,
limit
:
5
});
var
feedPath
=
listPath
(
'index'
,
'recent-posts'
,{
descending
:
true
,
limit
:
5
,
format
:
"atom"
});
The next two lines of the function generate URLs used to link to
the index page itself, as well as the XML Atom feed version of it. The
listPath
function is defined in
path.js—the upshot is that it knows how to link to
lists generated by the same design document it is run from.
The next section of the function is responsible for rendering the
HTML output of the blog. Refer to Chapter 8 for
details about the API we use here. In short, clients can describe the
format(s) they prefer in the HTTP Accept header, or in a format
query
parameter. On the server, we declare which formats we provide,
as well as assign each format a priority. In cases where the client
accepts multiple formats, the first declared format is returned. It is
not uncommon for browsers to accept a wide range of formats, so take
care to put HTML at the top of the list, or else you can end up with
browsers receiving alternate formats when they expect HTML.
provides
(
"html"
,
function
()
{
The provides
function takes two arguments: the
name of the format (which is keyed to a list of default MIME types) and
a function to execute when rendering that format. Note that when using
provides
, all send
and
getRow
calls must happen within the render function.
Now let’s look at how the HTML is actually generated.
send
(
template
(
templates
.
index
.
head
,
{
title
:
blog
.
title
,
feedPath
:
feedPath
,
newPostPath
:
showPath
(
"edit"
),
index
:
indexPath
,
assets
:
assetPath
()
}));
The first thing we see is a template being run with an object that
contains the blog title and a few relative URLs. The template function
used by Sofa is fairly simple; it just replaces some parts of the
template string with passed in values. In this case, the template string
is stored in the variable templates.index.head
, which was
imported using a CouchApp macro at the top of the function. The second
argument to the template function are the values that will be inserted
into the template; in this case, title
,
feedPath
, newPostPath
,
index
, and assets
. We’ll look at
the template itself later in this
chapter. For now, it’s sufficient to know that the template
stored in templates.index.head
renders the topmost portion of the HTML page, which does not change
regardless of the contents of our recent posts view.
Now that we have rendered the top of the page, it’s time to loop over the blog posts, rendering them one at a time. The first thing we do is declare our variables and our loop:
var
row
,
key
;
while
(
row
=
getRow
())
{
var
post
=
row
.
value
;
key
=
row
.
key
;
The row
variable is used to store each JSON
view row as it is sent to our function. The key
variable plays a different role. Because we don’t know ahead of time
which of our rows will be the last row to be processed, we keep the key
available in its own variable, to be used after all rows are rendered,
to generate the link to the next page of results.
send
(
template
(
templates
.
index
.
row
,
{
title
:
post
.
title
,
summary
:
post
.
summary
,
date
:
post
.
created_at
,
link
:
showPath
(
'post'
,
row
.
id
)
}));
}
Now that we have the row and its key safely stored, we use the
template engine again for rendering. This time we use the template
stored in templates.index.row
, with a data item that
includes the blog post title, a URL for its page, the summary of the
blog post we generated in our map view, and the date the post was
created.
Once all the blog posts included in the view result have been
listed, we’re ready to close the list and finish rendering the page. The
last string does not need to be sent to the client using
send()
, but it can be returned from the HTML
function. Aside from that minor detail, rendering the tail template
should be familiar by now.
return
template
(
templates
.
index
.
tail
,
{
assets
:
assetPath
(),
older
:
olderPath
(
key
)
});
});
Once the tail has been returned, we close the HTML generating function. If we didn’t care to offer an Atom feed of our blog, we’d be done here. But we know most readers are going to be accessing the blog through a feed reader or some kind of syndication, so an Atom feed is crucial.
provides
(
"atom"
,
function
()
{
The Atom generation function is defined in just the same way as
the HTML generation function—by being passed to
provides()
with a label describing the format it
outputs. The general pattern of the Atom function is the same as the
HTML function: output the first section of the feed, then output the
feed entries, and finally close the feed.
// we load the first row to find the most recent change date
var
row
=
getRow
();
One difference is that for the Atom feed, we need to know when it
was last changed. This will normally be the time at which the first item
in the feed was changed, so we load the first row before outputting any
data to the client (other than HTTP headers, which are set when the
provides
function picks the format). Now that we have
the first row, we can use the date from it to set the Atom feed’s
last-updated field.
// generate the feed header
var
feedHeader
=
Atom
.
header
({
updated
:
(
row
?
new
Date
(
row
.
value
.
created_at
)
:
new
Date
()),
title
:
blog
.
title
,
feed_id
:
makeAbsolute
(
req
,
indexPath
),
feed_link
:
makeAbsolute
(
req
,
feedPath
),
});
The Atom.header
function is defined in
lib/atom.js, which was imported by CouchApp at the
top of our function. This library uses JavaScript’s E4X extension to
generate feed XML.
// send the header to the client
send
(
feedHeader
);
Once the feed header has been generated, sending it to the client
uses the familiar send()
call. Now that we’re done
with the header, we’ll generate each Atom entry, based on a row in the
view. We use a slightly different loop format in this case than in the
HTML case, as we’ve already loaded the first row in order to use its
timestamp in the feed header.
// loop over all rows
if
(
row
)
{
do
{
The JavaScript do
/while
loop
is similar to the while
loop used in the HTML
function, except that it’s guaranteed to run at least once, as it
evaluates the conditional statement after each iteration. This means we
can output an entry for the row we’ve already loaded, before calling
getRow()
to load the next entry.
// generate the entry for this row
var
feedEntry
=
Atom
.
entry
({
entry_id
:
makeAbsolute
(
req
,
'/'
+
encodeURIComponent
(
req
.
info
.
db_name
)
+
'/'
+
encodeURIComponent
(
row
.
id
)),
title
:
row
.
value
.
title
,
content
:
row
.
value
.
html
,
updated
:
new
Date
(
row
.
value
.
created_at
),
author
:
row
.
value
.
author
,
alternate
:
makeAbsolute
(
req
,
showPath
(
'post'
,
row
.
id
))
});
// send the entry to client
send
(
feedEntry
);
Rendering the entries also uses the Atom library in
atom.js. The big difference between the Atom
entries and the list items in HTML, is that for our HTML screen we only
output the summary of the entry text, but for the Atom entries we output
the entire entry. By changing the value of content
from row.value.html
to
row.value.summary
, you could change the Atom feed to
only include shortened post summaries, forcing subscribers to click
through to the actual post to read it.
}
while
(
row
=
getRow
());
}
As we mentioned earlier, this loop construct puts the loop condition at the end of the loop, so here is where we load the next row of the loop.
// close the loop after all rows are rendered
return
"</feed>"
;
});
};
Once all rows have been looped over, we end the feed by returning the closing XML tag to the client as the last chunk of data.
Figure 14-1 shows the final result.
This is our final list of blog posts. That wasn’t too hard, was it? We now have the front page of the blog, we know how to query single documents as well as views, and we know how to pass arguments to views.
Get CouchDB: The Definitive Guide now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.