Chapter 4. HTML5 Hypermedia
The only usefulness of a map or a language depends on the similarity of structure between the empirical world and the map-languages.
- Alfred Korzybski
This last hands-on design chapter covers the techniques involved when using HTML5 as the base media type for your design. Since HTML already has a rich set of hypermedia controls (A, LINK, FORM, INPUT, etc.), the process of designing hypermedia types with HTML focuses on expressing the domain semantics within the existing elements and attributes of HTML.
The scenario here involves expressing common microblogging semantics (Twitter, Identi.ca, etc.) in a hypermedia type. After capturing the semantics and completing the design, a simple server will be built that emits the hypermedia responses along with two distinct client implementations. One client relies on only the HTML responses (no JavaScript support), and the other takes advantage of the Ajax style to build a web bot that knows enough about the hypermedia type to register as a new user (if needed) and begin posting into the data stream.
Scenario
For this example, a functional microblog hypermedia type is needed. This design should allow users to:
Create, update, and delete accounts
Add new messages and share existing messages
Create and remove relationships to other users
Search for existing users and messages
Since the design will be based on HTML5 (an existing hypermedia type), the implementation should provide basic functionality without the need for client-side scripting (e.g. JavaScript). However, the design should also allow scripted clients (e.g. Ajax-style browser clients) to implement a rich user interface, too. As in the previous examples, data will be stored using CouchDB, the server will be implemented using Node.js, and the client applications will be implemented using HTML5 and JavaScript.
Designing the Microblog Media Type
The aim of this example is to illustrate the process of designing hypermedia types using HTML5 as the base format. Unlike the previous examples, which were based on XML and JSON (both formats devoid of native hypermedia controls), this example starts with a format that already supports basic hypermedia factors.
Starting with HTML5 provides both advantages and drawbacks when designing a hypermedia solution. The good news is the details of document design (which elements are valid, etc.), the specific hypermedia controls, and the method of expressing state transitions are already decided. This means designers do not need to worry about how to define and document much of the hypermedia type. However, with so many details already decided, there is less room for expressing the problem domain in ways clients can easily understand.
This duality of well-defined hypermedia controls and a limit on the options for expressing the problem domain is the key challenge behind working with existing hypermedia types and is, in part, the reason so few new APIs use HTML5 as the base. For many, it seems easier to start from zero using a non-hypermedia type such as XML or JSON and create all of the details from the beginning. But sometimes this option is not available to designers. Instead, designers often must be sure their implementations work well with existing clients (e.g. web browsers) with little or no support from custom scripting or other client-side plug-ins.
Expressing Application Domain Semantics in HTML5
In cases where the design needs to rely on existing hypermedia
types like HTML5, a different approach is needed to express domain
semantics. Instead of creating new elements (as in XML) or objects (as
in JSON), domain semantics must be expressed using existing HTML5
elements (<article>
, <section>
,
<p>
, <span>
, etc.). Instead of
creating new state transition and process flow elements, designers need
to use those already available in HTML5 (<form>
,
<input>
, <a>
, etc.).
However, it is still important to know the semantic value (the
meaning in relation to the problem domain) of each of these
already-defined elements. Instead of creating an
<email>
element or <update-user>
transition block, designers need to use existing features of HTML5 to
add semantic meaning to the representations. The way to do this is to
use key HTML5 attributes to decorate the existing elements. These
attributes are:
id
Used to identify a unique data element/block within the representation
name
Used to identify a state transition element (i.e. input) within the representation
class
Used to identify a non-unique data element/block within the representation
rel
Used to identify a non-unique process flow element within the representation
The id
attribute can be applied to any element in the
document, is a single string value (no spaces allowed), and must be
unique in a document:
<span id="invoice-001">...</span>
The name
attribute is also a single string value
(without spaces). It can be assigned to a limited set of elements (those
associated with input), and multiple elements within the same document
can share the same value:
<label>Enter first invioce:</label><input name="invoice">...</input> <label>Enter second invoice:</label><input name="invoice">...</input>
The class
attribute can be applied to any element in
the document, can appear on several elements within the document, and
allows multiple values to be applied as long as they are separated by a
space. This makes it possible to mark several document elements with the
same value as well as mark the same element with several values:
<span class="important invoice overdue">...</span> <span class="invoice pending">...</span>
The rel
attribute has features of both the
class
and name
attributes. Like the
name
attribute, a rel
can only appear on a
limited set of elements (those associated with links) and need not be
unique within the document. Also, like the class
attribute,
the rel
supports multiple strings separated by
spaces:
<a href="..." rel="invoice">...</a> <a href="..." rel="invoice overdue">...</a>
Through the proper application of these four attributes to a wide range of existing HTML5 elements, it is possible to adequately express any problem domain details within a hypermedia type design. It is worth noting that each of the above attributes has slightly different rules in HTML5.
Identifying the State Transitions
Just as in other base formats, a key step in implementing a hypermedia design using HTML5 is the process of identifying the needed state transitions. As has already been stated above, this example implements the basic features of a microblogging application. Below is a set of states that need to be represented:
The list of users
A single user
The list of messages
A single message
The list of possible queries (search users, view the user’s list of followers, etc.)
A template for creating a new user
A template for updating an existing user
A template for following an existing user
A template for searching for existing users
A template for adding a new message
A template for replying to an existing message
A template for searching for existing messages
As may be evident to the reader at this point, the above list
identifies three unique block types within a representation (users,
messages, queries) and seven transition blocks (create user, update
user, follow user, search user, add message, reply to a message, and
search messages). The unique blocks can be expressed using HTML5’s
<div>
element and the transition blocks can be
expressed using the <form>
element. Each of these
blocks will have a number of child elements.
Note
The usual error representation has been left out of this design to save time and reduce repetition of the same material. When creating a complete design for production use, you should always include an error representation.
The details of these element blocks can easily be expressed in short HTML5 examples. These examples will serve as reference material when creating the application profile later in this chapter (see The Microblog Application Profile).
State blocks
This example identifies three main types of content that may
appear within a representation: users, messages, and queries. These
can be expressed as HTML5 <div>
elements with
unordered lists (<ul>
, <li>
) and
other child elements. Below are examples of each of the three main
content blocks.
Users
There are two ways to represent users within this design: as a list of users and as a single user.
The list of users is represented using an HTML5 unordered list:
<!-- representing a list of users --> <div id="users"> <ul class="all"> <li> <span class="user-text">User1</span> <a rel="user" href="..." title="profile for User1">profile</a> <a rel="messages" href="..." title="messages by User1">messages</a> </li> ... <li> <span class="user-text">UserN</span> <a rel="user" href="..." title="profile for UserN">profile</a> <a rel="messages" href="..." title="messages by UserN">messages</a> </li> </ul> </div>
A single user is represented in a similar way:
<!-- representing a single user --> <div id="users"> <ul class="single"> <li> <img class="user-image" src="..." alt="image of User1"/> <a rel="user" href="..." title="profile for User1"> <span class="user-text">User One</span> </a> <span class="description"> I am known to all as User One </span> <a rel="website" href="..." title="website"> User1's web site </a> <a rel="messages" href="..." title="messages by User1">messages</a> </li> </ul> </div>
Messages
Messages can also be represented in two ways (as a list and as a single message).
Here is the list representation:
<!-- representing a list of messages --> <div id="messages"> <ul class="all"> <li> <span class="message-text"> this is a message </span> @ <a rel="message" href="..." title="message"> <span class="date-time"> 2011-08-17 04:04:09 </span> </a> by <a rel="user" href="..." title="User1"> <span class="user-text">User1</span> </a> </li> ... <li> <span class="message-text"> this is also a message </span> @ <a rel="message" href="..." title="message"> <span class="date-time"> 2011-08-17 04:10:13 </span> </a> by <a rel="user" href="..." title="UserN"> <span class="user-text">UserN</span> </a> </li> </ul> </div>
And here is the way a single message is represented:
<!-- representing a single message --> <div id="messages"> <ul class="single"> <li> <span class="message-text"> This is a message </span> <span class="single">@</span> <a rel="message" href="..." title="message"> <span class="date-time"> 2011-08-17 04:04:09 </span> </a> <span class="single">by</span> <a rel="user" href="..." title="User1"> <span class="user-text">User1</span> </a> </li> </ul> </div>
Queries
This design uses a small set of simple queries. These are just links that return the list of messages, users, or the registration page:
<!-- representing the queries list --> <div id="queries"> <ul> <li><a rel="messages-all index" href="..." title="Home page">Home</a></li> <li><a rel="users-all" href="..." title="User List">Users</a></li> <li><a rel="register" href="..." title="Register">Register</a></li> </ul> </div>
Note that the first link in the list has two values for the
rel
attribute.
Transfer blocks
This example identifies seven client-initiated state transfers.
Each of them can be expressed as HTML5 <form>
elements with <input>
child elements. Below are
each of the transfer blocks along with the details for each child
element.
Create new user
The server will include this block in the representation to allow clients to create a new microblog user:
<!-- state transfer for adding a new user --> <form method="post" action="..." class="user-add"> <input type="text" name="name" value="" required="true"/> <input type="text" name="email" value="" required="true"/> <input type="password" name="password" value="" required="true" /> <textarea name="description"></textarea> <input type="file" name="avatar" value="" /> <input type="text" name="website" value="" /> <input type="submit" value="Send" /> </form>
Notice that some fields are grouped under the SHOULD keyword and others are under the MAY keyword. These are just comments to indicate which fields “should” be returned by the server and which ones “may” be returned by the server. These annotations will be used later when writing up the documentation.
Update existing user
Once a user has been created, it should be possible to update that user account:
<!-- state transfer for updating an existing user --> <form method="post" action="..." class="user-update"> <input type="text" name="name" value="" required="true"/> <input type="text" name="email" value="" required="true"/> <input type="password" name="password" value="" required="true"/> <textarea name="description"></textarea> <input type="file" name="avatar" value="" /> <input type="text" name="website" value="" /> <input type="submit" value="Send" /> </form>
It is likely that update transitions will be prepopulated with
existing data. It is also possible that one or more of the
<input />
elements will be marked as read-only by
the server. These details will be left up to each server
implementation to determine.
Follow a user
Following a user is as simple as sending the identifier of the user you wish to follow:
<!-- state transition to follow an existing user --> <form method="post" action="..." class="user-follow"> <input type="text" name="user" value="" required="true"/> <input type="submit" value="Send" /> </form>
You may notice that there is nothing in the above transition to indicate the current user that wants to follow the identity in the user input element. For all of the transition examples shown here, the server is assumed to be able to know (when it is important) the identity of the current (or logged-in) user. This can be done via information in the action URI or via control data (HTTP Headers) such as the Authorization header or the Cookie header (see Current user and state data for details).
Search for users
Here is the transition block for searching for users:
<!-- state transfer for searching users --> <form method="get" action="..." class="user-search"> <input type="text" name="search" value="..." required="true"/> <input type="submit" value="Send" /> </form>
Since this is a search action, the method attribute is set to GET, not POST.
Add a new message
Adding a new message is also a simple state transfer:
<!-- state transfer for adding a message --> <form method="post" action="..." class="message-post"> <textarea name="message" requried="true"></textarea> <input type="submit" value="Send" /> </form>
Reply to an existing message
Replying to an existing message involves sending both the original user identifier and any reply text, which may include the original message text, to the server:
<!-- state transfer for replying to a message --> <form method="post" action="..." class="message-reply"> <input type="hidden" name="user" value="..." requried="true"/> <textarea name="message"></textarea> <input type="submit" value="Send" /> </form>
Search for messages
The message search transfer block is almost identical to the one used for user searches:
<!-- state transfer for searching messages --> <form method="get" action="..." class="message-search"> <input type="text" name="search" value="..." requried="true"/> <input type="submit" value="Send" /> </form>
Selecting the Basic Design Elements
Since the point of this chapter is to cover implementation details when using HTML5, the remaining standard design elements have already been decided. However, it is still valuable to go over each of them here.
Using HTML5 as the base format means that the State Transfer style will be ad hoc. HTML5 Forms will be used to make all client-imitated transfers via GET (read-only) or POST (add/update). Even though the ad hoc style is used in HTML5, designers can still establish required and optional inputs in their hypermedia design. In this example, the design uses both.
The domain style of HTML5 is agnostic. The element and attribute
names are all independent of any domain semantics. However, as mentioned
earlier in this chapter (see Expressing Application Domain Semantics in HTML5), HTML5 supports domain
semantic expression using common attribute values (id
,
name
, class
). Finally, the Application Flow
style for HTML5 is applied via values for the rel
attribute
on href
tags.
Note
Usually hypermedia designs use the rel
attribute
(or its equivalent in the data format) to mark all state transitions,
including ones that require arguments. However, HTML5 does not support
the rel
attribute on the form
element. For
this reason, transitions that require arguments (e.g. forms) will be
marked with a class
attribute instead.
Although HTML5 is a domain-agnostic base format, the built-in
state transfer and application flow elements make most of the design
details very easy. The only creative work that needs to be done is
arranging existing HTML5 elements (article
,
section
, div
, p
,
span
, etc.) and decorating them with the necessary
attributes (id
, name
, class
,
rel
).
The application of these attribute decorations is covered in the next section.
The Microblog Application Profile
Since HTML5 is a domain-agnostic media type, all domain-specific
information (both the data elements and the transition details) needs to
be specified as additional information in each representation. As has
already been pointed out earlier in this chapter (see Expressing Application Domain Semantics in HTML5), in HTML5 you can
express domain-specific information using a set of attributes
(id
, name
, class
, and
rel
).
Also, this implementation will require users to log in before posting new messages. That means the implementation will need to be able to identify the current logged-in user.
Current user and state data
This example implementation will rely on HTTP’s Authorization header to identify the currently logged-in user. That means this example server will use HTTP Basic Authentication for selected state transitions (Update a User, Follow a User, Add a New Message, and Reply to a Message). It is important to point out that user identification is not specified in the media type design; it is an implementation detail left to servers and clients to work out themselves.
The decision to leave user authentication independent of the
media type has a number of advantages. First, this allows clients and
servers to negotiate for an appropriate authentication scheme at
runtime (the HTTP WWW-Authenticate
head is used to
advertise supported authentication schemes). Second, leaving it out of
the media type means that servers are free to establish and transition
details on their own. For example, Open Auth (OAuth) has a set of
requirements for interacting with more than one web server in order to
complete authentication. Finally, leaving authentication details out
of the media type design allows servers to take advantage of whatever
means may become available in the future.
ID attribute values
This design relies on three unique identifiers for representations:
- messages
Applied to a
div
tag. The list of messages in this representation. This list may contain only one message.- queries
Applied to a
div
tag. The list of valid queries in this representation. This is a list of simple queries (represented by the HTML anchor tag).- users
Applied to a
div
tag. The list of users in this representation. This list may contain only one user.
Class attribute values
There are a number of class
attributes that can
appear within a representation. Clients should be prepared to
recognize the following values:
- all
Applied to a
UL
tag. A list representation. When this element is a descendant ofDIV.id="messages"
it MAY have one or moreLI.class="message"
descendant elements. When this element is a descendant ofDIV.id="users
" it MAY have one or moreLI.class="user"
descendant elements.- date-time
Applied to a
SPAN
tag. Contains the UTC date-time the message was posted. When present, it SHOULD be valid per RFC3339.- description
Applied to a
SPAN
tag. Contains the text description of a user.- friends
Applied to a
UL
tag. A list representation. When this element is a descendant ofDIV.id="messages
it contains the list of messages posted by the designated user’s friends and MAY have one or more"
LI.class="message"
descendant elements. When this element is a descendant ofDIV.id="users"
it contains the list of users who are the friends of the designated user and MAY have one or moreLI.class="user"
descendant elements.- followers
Applied to a
UL
tag. A list representation of all the users from the designated user’s friends list. MAY have one or moreLI.class="user"
descendant elements.- me
Applied to a
UL
tag. When this element is a descendant ofDIV.id="messages"
it contains the list of messages posted by the designated user and MAY have one or moreLI.class="message"
descendant elements. When this element is a descendant ofDIV.id="users"
it SHOULD contain a single descendantLI.class="user"
with the designated user’s profile.- mentions
Applied to a
UL
tag. A list representation of all the messages that mention the designated user. It MAY contain one or moreLI.class="message"
descendant elements.- message
Applied to an
LI
tag. A representation of a single message. It SHOULD contain the following descendant elements:SPAN.class="user-text"
A.rel="user"
SPAN.class="message-text"
A.rel="message"
It MAY also contain the following descendant elements:
IMG.class="user-image"
SPAN.class="date-time"
- message-post
Applied to a
FORM
tag. A link template to add a new message to the system by the designated (logged-in) user. The element MUST be set toFORM.method="post"
and SHOULD contain a descendant element:TEXTAREA.name="message"
- message-reply
Applied to a
FORM
tag. A link template to reply to an existing message. The element MUST be set toFORM.method="post"
and SHOULD contain the following descendant elements:INPUT[hidden].name="user"
(the author of the original post)TEXTAREA.name="message"
- single
When this element is a descendant of
DIV.id="messages"
it contains the message selected via a message link. SHOULD have a singleLI.class="message"
descendant element. When this element is a descendant ofDIV.id="users"
it contains the user selected via a user link. SHOULD have a singleLI.class="user"
descendant element.- messages-search
Applied to a
FORM
tag. A link template to search of all the messages. The element MUST be set toFORM.method="get"
and SHOULD contain the following descendant elements:INPUT[text].name="search"
- message-text
Applied to a
SPAN
tag. The text of a message posted by a user.- search
Applied to a
UL
tag. A list representation. When this element is a descendant ofDIV.id="messages"
it contains a list of messages and MAY have one or moreLI.class="message"
descendant elements. When this element is a descendant ofDIV.id="users"
it contains a list of users and MAY have one or moreLI.class="user"
descendant elements.- shares
Applied to a
UL
tag. A list representation of all the messages posted by the designated user that were shared by other users. It MAY contain one or moreLI.class="message"
descendant elements.- user
Applied to an
LI
tag. A representation of a single user. It SHOULD contain the following descendant elements:SPAN.class="user-text"
A.rel="user"
A.rel="messages"
It MAY also contain the following descendant elements:
SPAN.class="description"
IMG.class="avatar"
A.rel="website"
- user-add
Applied to a
FORM
tag. A link template to create a new user profile. The element MUST be set toFORM.method="post"
and SHOULD contain the following descendant elements:INPUT[text].name="user"
INPUT[text].name="email"
INPUT[password].name="password"
It MAY also contain the following descendant elements:
TEXTAREA.name="description"
INPUT[file].name="avatar"
INPUT[text].name="website"
- user-follow
Applied to a
FORM
tag. A link template to add a user to the designated user’s friend list. The element MUST be set toFORM.method="post"
and SHOULD contain the descendant element:INPUT[text].name="user"
- user-image
Applied to an
IMG
tag. A reference to an image of the designated user.- user-text
Applied to a
SPAN
tag. The user nickname text.- user-update
Applied to a
FORM
tag. A link template to update the designated user’s profile. The element MUST be set toFORM.method="post"
and SHOULD contain the following descendant elements:INPUT[hidden].name="user"
INPUT[hidden].name="email"
INPUT[password].name="password"
It MAY also contain the following descendant elements:
TEXTAREA.name="description"
INPUT[file].name="avatar"
INPUT[text].name="website"
- users-search
Applied to a
FORM
tag. A link template to search of all the users. The element MUST be set toFORM.method="get"
and SHOULD contain the descendant element:INPUT[text].name="search"
Name attributes values
HTML5 uses the name
attribute to identify a
representation element that will be used to supply data to the server
during a state transition. Clients should be prepared to supply values
for the following state transition elements:
- description
Applied to a TEXTAREA element. The description of the user.
Applied to an INPUT[text] or INPUT[hidden] element. The email address of a user. When supplied, it SHOULD be valid per RFC5322.
- message
Applied to a TEXTAREA element. The message to post (for the designated user).
- name
Applied to an INPUT[text] element. The (full) name of a user.
- password
Applied to an INPUT[password] element. The password of the user login.
- search
Applied to an INPUT[text]. The search value to use when searching messages (when applied to
FORM.class="message-search"
) or when searching users (when applied toFORM.class="users-search"
).- user
Applied to an INPUT[text] or INPUT[hidden] element. The public nickname of a user.
- avatar
Applied to an INPUT[file] element. The image for the user.
- website
Applied to an INPUT[text]. The URL of a website associated with the user profile. When supplied, it SHOULD be valid per RFC 3986.
Rel attribute values
This design also identifies a number of possible simple state
transitions, or static links, that may appear within representations.
These will appear as HTML anchor tags with the following
rel
attribute values:
- index
Applied to an A tag. A reference to the starting URI for the application.
- message
Applied to an A tag. A reference to a message representation.
- message-post
Applied to an A tag. A reference to the message-post FORM.
- message-reply
Applied to an A tag. A reference to the message-reply FORM.
- message-share
Applied to an A tag. A reference to the message-share FORM.
- messages-all
Applied to an A tag. A reference to a list representation of all the messages in the system.
- messages-search
Applied to an A tag. A reference to the messages-search FORM.
- user
Applied to an A tag. A reference to a user representation.
- user-add
Applied to an A tag. A reference to the user-add FORM.
- user-follow
Applied to an A tag. A reference to the user-follow FORM.
- user-update
Applied to an A tag. A reference to the user-update FORM.
- users-all
Applied to an A tag. A reference to a list representation of all the users in the system.
- users-friends
Applied to an A tag. A reference to list representation of the designated user’s friend users.
- users-followers
Applied to an A tag. A reference to list representation of the users who follow the designated user.
- users-search
Applied to an A tag. A reference to the users-search FORM.
- website
Applied to an A tag. A reference to the website associated with a user.
Sample Data
The test data for this example design can be represented using three different documents in CouchDB: the User, Message, and Follows documents. This implementation also relies on a CouchDB design document that includes a handful of views and a validation routine for writing documents into the data store.
User Documents
For this implementation, users will be represented in the data store as follows:
{ "_id" : "mamund", "type" : "user", "name" : "Mike Amundsen", "email" : "mamund@yahoo.com", "password" : "p@ssW0rd", "description" : "learnin hypermedia", "imageUrl" : "http://amundsen.com/images/mca-photos/mca-icon-b.jpg", "websiteUrl" : "http://amundsen.com", "dateCreated" : "2011-06-21" }
Message Documents
Each message document in the data store looks like this:
{ "type" : "post", "text" : "My first message!", "user" : "mamund", "dateCreated" : "2011-06-29" }
Follow Documents
The system will also track which users follow each other using a Follow document:
{ "type" : "follow", "user" : "mamund", "follows" : "lee" }
Design Document
The predefined views and validation routines for the CouchDB data for this implementation are:
{ "_id" : "_design/microblog", "views" : { "users_search" : { "map" : "function(doc){ if(doc._id && doc.type==='user') { emit(doc._id,doc); } }" }, "users_by_id" : { "map" : "function(doc){ if(doc._id && doc.type==='user') { emit(doc._id,doc); } }" }, "posts_all" : { "map" : "function(doc) { if(doc._id && doc.type==='post') { emit(doc.dateCreated.split('-'),doc); } }" }, "posts_by_id" : { "map" : "function(doc) { if(doc._id && doc.type==='post') { emit(doc._id,doc); } }" }, "posts_by_user" : { "map" : "function(doc) { if(doc._id && doc.type==='post') { emit(doc.user, doc); } }" }, "posts_search" : { "map" : "function(doc) { if(doc._id && doc.type==='post') { emit(doc.user, doc); } }" }, "posts_by_user" : { "map" : "function(doc) { if(doc.user && doc.type==='post') { emit(doc.dateCreated.split('-').concat(doc.user),doc); } }" }, "follows_user_is_following" : { "map" : "function(doc) { if(doc.user && doc.type==='follow') { emit(doc.user, {_id:doc.follows}); } }" }, "follows_is_following_user" : { "map" : "function(doc) { if(doc.follows && doc.type==='follow') { emit(doc.follows, {_id:doc.user}); } }" } }, "validate_doc_update": "function(newDoc, oldDoc, userCtx) { function require(field, message) { message = message || field + ' is required'; if (!newDoc[field]) { throw({forbidden : message}); } }; function unchanged(field) { if(oldDoc && toJSON(oldDoc[field]) !== toJSON(newDoc[field])) { throw({forbidden : field + ' is read-only'}); } }; if(newDoc._deleted) { return true; } else { switch(newDoc.type) { case 'user': require('name'); require('email'); require('password'); break; case 'post': require('text'); require('user'); require('dateCreated'); break; case 'follow': require('user'); require('follows'); break; } } }" }
The Server Code
With sample data in place, the next step is to implement server code to test the design. Since this example application has quite a few state transitions, the server-side code is a bit more involved than previous examples. However, an advantage of using HTML5 as the base media type is that it is rather easy to test since the server output will easily render within common web browsers.
Authenticating Users
This implementation relies on HTTP Basic Authentication to identify users. Below is the top-level routine to handle requesting the username and password and then comparing the results to information stored in a CouchDB User document:
/* validate user (from db) via HTTP Basic Auth */ function validateUser(req, res, next) { var parts, auth, scheme, credentials; var view, options; // handle auth stuff auth = req.headers["authorization"]; if (!auth){ return authRequired(res, 'Microblog'); } parts = auth.split(' '); scheme = parts[0] credentials = new Buffer(parts[1], 'base64').toString().split(':'); if ('Basic' != scheme) { return badRequest(res); } req.credentials = credentials; // ok, let's look this user up view = '/_design/microblog/_view/users_by_id'; options = {}; options.descending='true'; options.key=String.fromCharCode(34)+req.credentials[0]+String.fromCharCode(34);; db.get(view, options, function(err, doc) { try { if(doc[0].value.password===req.credentials[1]) { next(req,res); } else { throw new Error('Invalid User'); } } catch (ex) { return authRequired(res, 'Microblog'); } }); };
In the above routine, the code first checks for the presence of
the Authorization
header. If it does not exist, the server
sends a response that asks the client to supply credentials before
continuing (the authRequired
method call). Once an
Authorization
header is supplied by the client, the code
first ensures that it uses the Basic authentication scheme and then
proceeds to parse the header into its two key parts
(username:password
). The first part (username
)
is used to perform a look up against the data store and, if a record is
found, the second part (password
) is compared against the
data store in the User document. If there is a match, then the user has
been authenticated and the code execution continues as usual (the
next
method call).
Registering a New User
Since this implementation supports adding new users, there is a view for rendering the state transition form and code to handle both returning the form and processing the posted data (using POST).
First, here is the code to return the input form:
/* get user register page */ app.get('/microblog/register/', function(req, res){ res.header('content-type',contentType); res.render('register', { title: 'Register', site: baseUrl, }); });
And the view template to render that form:
<h2 id="page-title"><%= title %></h2> <form class="user-add" action="<%=site%>users/" method="post"> <fieldset> <h4>Account</h4> <p class="input"> <label>Handle:</label> <input name="user" value="" placeholder="coolhandle" required="true"/> </p> <p class="input"> <label>Password:</label> <input name="password" type="password" value="" placeholder="mys3cr3t" required="true" /> </p> </fieldset> <fieldset> <h4>User Info</h4> <p class="input"> <label>Email Address:</label> <input name="email" type="email" value="" placeholder="user@example.com" required="true"/> </p> <p class="input"> <label>Full Name:</label> <input name="name" value="" placeholder="Jane Doe"/> </p> <p class="input"> <label>Description:</label> <textarea name="description"></textarea> </p> <p class="input"> <label>Avatar URL:</label> <input name="avatar" type="url" value="" placeholder="http://example.com/images/my-avatar.jpg"/> </p> <p class="input"> <label>Website URL:</label> <input name="website" type="url" value="" placeholder="http://example.com/my-blog/"/> </p> </fieldset> <p class="buttons"> <input type="submit" value="Submit" /> <input type="reset" value="Reset" /> </p> </form>
In the above template, you can see that some fields are marked as
required="true"
and some have placeholder
values included to give users a hint on how to fill out the various
inputs.
Below is the server code that runs when users submit the state transition form:
/* post to user list page */ app.post('/microblog/users/', function(req, res) { var item,id; id = req.body.user; if(id==='') { res.status=400; res.send('missing user'); } else { item = {}; item.type='user'; item.password = req.body.password; item.name = req.body.name; item.email = req.body.email; item.description = req.body.description item.imageUrl = req.body.avatar; item.websiteUrl = req.body.website; item.dateCreated = today(); // write to DB db.save(req.body.user, item, function(err, doc) { if(err) { res.status=400; res.send(err); } else { res.redirect('/microblog/users/', 302); } }); } });
Message Responses
There are two responses for representing messages: the message list and message details. There is also a state transition for adding a new message to the data store.
Below is the code that returns the message list:
/* starting page */ app.get('/microblog/', function(req, res){ var view = '/_design/microblog/_view/posts_all'; var options = {}; options.descending = 'true'; ctype = acceptsXml(req); db.get(view, options, function(err, doc) { res.header('content-type',ctype); res.render('index', { title: 'Home', site: baseUrl, items: doc }); }); });
And the view template that renders the message list:
<h2 id="page-title"><%= title %></h2> <form class="message-post" action="<%=site%>messages/" method="post"> <fieldset> <h4>What's Up?</h4> <textarea name="message" cols="50" rows="1" size="140" required="true"></textarea> <span class="message-buttons"> <input type="submit" value="Submit" /> <input type="reset" value="Reset" /> </span> </fieldset> </form> <div id="messages"> <ul class="all"> <% for(i=0,x=items.length;i<x;i++) { %> <li> <span class="message-text"> <%=items[i].value.text%> </span> @ <a rel="message" href="<%=site%>messages/<%=items[i].value._id%>" title="message"> <span class="date-time"> <%=items[i].value.dateCreated%> </span> </a> by <a rel="user" href="<%=site%>users/<%=items[i].value.user%>" title="<%=items[i].value.user%>"> <span class="user-text"><%=items[i].value.user%></span> </a> </li> <% } %> </ul> </div>
Note that this view template also includes the state transition
block for adding a new message to the system
(message-post
). Below is the server code that handles the
message-post state transition:
// add a message app.post('/microblog/messages/', function(req, res) { validateUser(req, res, function(req,res) { var text; // get data array text = req.body.message; if(text!=='') { item = {}; item.type='post'; item.text = text; item.user = req.credentials[0]; item.dateCreated = now(); // write to DB db.save(item, function(err, doc) { if(err) { res.status=400; res.send(err); } else { res.redirect('/microblog/', 302); } }); } else { return badReqest(res); } }); });
Finally, here is the code to handle representing a single message.
This can be reached by activating the rel="message"
links
in the message list:
/* single message page */ app.get('/microblog/messages/:i', function(req, res){ var view, options, id; id = req.params.i; view = '/_design/microblog/_view/posts_by_id'; options = {}; options.descending='true'; options.key=String.fromCharCode(34)+id+String.fromCharCode(34); db.get(view, options, function(err, doc) { res.header('content-type',contentType); res.render('message', { title: id, site: baseUrl, items: doc }); }); });
Below is the template for rendering a single message response:
<div class="message-block"> <div id="messages"> <ul class="single"> <% for(i=0,x=items.length;i<x;i++) { %> <li> <span class="message-text"> <%=items[i].value.text%> </span> <span class="single">@</span> <a rel="message" href="<%=site%>messages/<%=items[i].value._id%>" title="message"> <span class="date-time"> <%=items[i].value.dateCreated%> </span> </a> <span class="single">by</span> <a rel="user" href="<%=site%>users/<%=items[i].value.user%>" title="<%=items[i].value.user%>"> <span class="user-text"><%=items[i].value.user%></span> </a> </li> <% } %> </ul> </div>   </div>
User Responses
This sample implementation has two representations for user responses: the user list and the user details. Below is the user list server code:
/* get user list page */ app.get('/microblog/users/', function(req, res){ var view = '/_design/microblog/_view/users_by_id'; db.get(view, function(err, doc) { res.header('content-type',contentType); res.render('users', { title: 'User List', site: baseUrl, items: doc }); }); });
And the user list view to match:
<h2 id="page-title"><%= title %></h2> <div id="users"> <ul class="all"> <% for(i=0,x=items.length;i<x;i++) { %> <li> <span class="user-text"><%= items[i].value.name %></span> <a rel="user" href="<%=site%>users/<%=items[i].value._id%>" title="profile for <%=items[i].value._id%>">profile</a> <a rel="messages" href="<%=site%>user-messages/<%=items[i].value._id%>" title="messages by <%=items[i].value._id%>">messages</a> </li> <% } %> </ul> </div>
There is also code to return a single user record:
/* single user profile page */ app.get('/microblog/users/:i', function(req, res){ var view, options, id; id = req.params.i; view = '/_design/microblog/_view/users_by_id'; options = {}; options.descending='true'; options.key=String.fromCharCode(34)+id+String.fromCharCode(34); db.get(view, options, function(err, doc) { res.header('content-type',contentType); res.render('user', { title: id, site: baseUrl, items: doc }); }); });
Along with the view template for rendering single user responses:
<h2 id="page-title"><%= title %></h2> <div id="users"> <ul class="single"> <li> <% if(items[0].value.imageUrl) { %> <img class="avatar" src="<%=items[0].value.imageUrl%>" /> <% } %> <a rel="user" href="<%=site%>users/<%=items[0].value._id%>" title="profile for <%=items[0].value._id%>"> <span class="user-text"><%= items[0].value.name %></span> </a> <% if(items[0].value.description) { %> <span class="description"> <%=items[0].value.description%> </span> <% } %> <% if(items[0].value.websiteUrl) { %> <a rel="website" href="<%=items[0].value.websiteUrl%>" title="website"> <%=items[0].value.websiteUrl%> </a> <% } %> <a rel="messages" href="<%=site%>user-messages/<%=items[0].value._id%>" title="messages by <%=items[0].value._id%>">messages</a> </li> </ul> </div>
Note that the single user template will optionally include the
user’s profile image (user-image
), description, and website
URL if they have been supplied. You should also notice that this
representation includes a link to see all of the messages created by
this user (rel="messages”). The server code and view template for that
response are:
/* user messages page */ app.get('/microblog/user-messages/:i', function(req, res){ var view, options, id; id = req.params.i; view = '/_design/microblog/_view/posts_by_user'; options = {}; options.descending='true'; options.key=String.fromCharCode(34)+id+String.fromCharCode(34);; db.get(view, options, function(err, doc) { res.header('content-type',contentType); res.render('user-messages', { title: id, site: baseUrl, items: doc }); }); });
<h2 id="page-title">Messages from <%=title%></h2> <div class="user-message-block"> <div id="messages"> <ul class="search"> <% for(i=0,x=items.length;i<x;i++) { %> <li> <span class="message-text"> <%=items[i].value.text%> </span> <span class="single">@</span> <a rel="message" href="<%=site%>messages/<%=items[i].value._id%>" title="message"> <span class="date-time"> <%=items[i].value.dateCreated%> </span> </a> <span class="single">by</span> <a rel="user" href="<%=site%>users/<%=items[i].value.user%>" title="<%=items[i].value.user%>"> <span class="user-text"><%=items[i].value.user%></span> </a> </li> <% } %> </ul> </div>   </div>
The Client Code
Once the server is implemented, it’s time to work through example clients. As was already mentioned, using HTML5 as the based media type means that the implementation will just run within common web browsers without any modification. However, plain HTML5 (without a Cascading Style Sheet, or CSS) is not very pleasing to the eye. It is a rather easy process to create a CSS stylesheet to spiff up plain HTML5 into a decent looking client. This results in a client that supports all of the required functionality without relying on any client-side scripting. This is sometimes called a Plain Old Semantic HTML or POSH client.
It is also possible to treat well-formed HTML5 as an XML document and render the responses using an Ajax-style user interface. This does, however, require that the HTML5 be rendered as valid XML. Lucky for our case, the view templates already meet this requirement.
The POSH Example
The basic HTML5 that is rendered by the server is fully functional, but a bit unappealing to view as can be seen in Figure 4-1.
However, with just a bit of CSS work, this view can be turned into a much more inviting user interface (see Figure 4-2).
The CSS file for this rendering includes rules for rendering the home page and message lists:
body { background-color: #FFFFCC; font-family: sans-serif; } div#queries { width:500px; float:right; } div#queries ul { margin:0; list-style-type:none; } div#queries ul li { float:left; padding-right: .4em; } div#messages ul { margin:0; list-style-type:none; } div#messages ul li { margin-top: .3em; } div#messages ul.single li span.message-text { font-size:large; font-weight:bold; } div.message-block { border: 1px solid black; padding:.5em; width:500px; margin:auto; -moz-border-radius: 25px; border-radius: 25px; } div#messages ul.single li a, div#messages ul.single li span.single { float:left; margin-right: .3em; } ul.single { margin:0; list-style-type:none; } ul.single a, ul.single span, ul.single img { display:block; }
The CSS document also includes details for rendering state transition blocks (HTML forms):
form.message-post { width:600px; } form.message-post textarea { display:block; float:left; } span.message-buttons { display:block; float:left; } fieldset { margin-top: 1em; -moz-border-radius: 25px; border-radius: 25px; background-color: #CCCC99; } fieldset h4 { margin:0; } form.user-add { width:300px; } p.input { margin-top:0; margin-bottom:0; } p.input label { display:block; width:150px; } p.input input, p.input textarea { float:left; width:200px; } span.message-buttons input { background-color: #ffffcc; -moz-border-radius: 15px; border-radius: 15px; } p.buttons input { background-color: #cccc99; -moz-border-radius: 15px; border-radius: 15px; }
Quite a bit more work could be done in this area, too. The CSS specification offers a wide range of options that make rendering POSH responses very easy and straightforward. The example here is given as just a starter for those who want to explore the area of user interface design via CSS.
The Ajax QuoteBot Example
In this example, a small Ajax client that knows enough about the microblogging hypermedia profile to interact with any server that supports this media type will be implemented. The bot shown here is able to determine if it needs to register as a new account on the server and then post quotes into the data stream at regular intervals. This example application shows that HTML can be successfully used as a machine-to-machine media type. It also provides guidance on one way to write stand-alone client applications for machine-to-machine scenarios.
Below is an example run of the QuoteBot Figure 4-3.
The QuoteBot scenario
For this example, the QuoteBot client will have the job of writing quotes to the microblogging server. If needed, this bot will also be able to register a new account on the target server. In order to accomplish these tasks, the QuoteBot will need to understand enough of the microblogging profile to perform a handful of low-level tasks such as loading the server’s home page, getting a list of users, finding and completing the user registration form, posting new messages, etc. Below is a list of these tasks along with notes on how the bot can use the microblogging profile specification to accomplish them:
Load an entry page on the server (the starting URI)
Get a list of registered users for this server (find the
@rel="users-all"
link)See if the bot is already registered (find the
@rel="user"
link with the value of the bot’s username)Load the user registration page (find the
@rel="register"
link)Find the user registration form (locate the
@class="user-add"
form on the page)Fill out the user registration form and submit it to the server (find the input fields outlined in the spec, populate them, and send the data to the server)
Load the message post page (
@rel="message-post"
link)Find the message post form (locate the
@class="message-post"
form on the page)Fill out the message post form and submit it to the server (find the input fields identified in the spec, populate, and send)
As the list above illustrates, the process of coding a machine-to-machine client for hypermedia interactions involves identifying two types of elements in response representations: links and forms. Clients need to be able to activate a link (i.e. follow the link) and fill in forms and activate them, too (i.e. submit the data to the server). Essentially, the client application needs to understand links and forms in general as well as the semantic details of the particular hypermedia profile.
QuoteBot HTML5
The HTML5 for the QuoteBot example is very minimal. All of the activity on the page is driven by the associated JavaScript. Below is the complete HTML5 markup:
<!DOCTYPE html> <html> <head> <title>MB QuoteBot</title> <script type="text/javascript" src="javascripts/base64.js"></script> <script type="text/javascript" src="javascripts/mbclient.js"></script> <style> h1 {margin:0;} h2,h3,h4 {margin-bottom:0;} p {margin-top:0;} div#side {float:left;margin:auto 1em auto auto;} div#main {float:left;} </style> </head> <body> <sidebar> <div id="side"> <img src="http://amundsen.com/images/robot.jpg" /> </div> </sidebar> <article> <div id="main"> <header> <h1>MB QuoteBot</h1> </header> <section> <p> This bot understands the Microblogging profile enough to register a new user account and start posting quotes to the stream. </p> </section> <section> <h3>Progress</h3> <p id="status"></p> </section> </div> <div id="output"> </div> </article> </body> </html>
QuoteBot JavaScript
The JavaScript for the QuoteBot looks as if it is complicated, but is actually rather simple. The code can be separated into a handful of sections:
- Setup
This code contains the initialization code, the variables used to fill out expected forms, and the list of quotes to send to the server.
- Making requests
This is a short bit of general code used to format and excecute an HTTP request. The code is smart enough to include a body and authentication information, if needed.
- Processing responses
This section contains all of the methods used to process the response representations from the server. This includes parsing the response, looking for links, and looking for and filling in forms. These routines either conclude with an additional request or, in case of an error, stop and report the status of the bot and stop.
- Supporting routines
These are utility functions to inspect arguments in the URI, format a URI for the next request, and look for elements in the response.
Setup code
The setup code includes details on shared state variables, details on forms to fill out, error messages, and a list of quotes to send to the server:
var g = {}; /* state values */ g.startUrl = '/microblog/' g.wait=10; g.status = ''; g.url = ''; g.body = ''; g.idx = 0; /* form@class="add-user" */ g.user = {}; g.user.user = 'robieBot5'; g.user.password = 'robie'; g.user.email = 'robie@example.org'; g.user.name = 'Robie the Robot'; g.user.description = 'a simple quote bot'; g.user.avatar = 'http://amundsen.com/images/robot.jpg'; g.user.website = 'http://robotstxt.org'; /* form@class="message-post" */ g.msg = {}; g.msg.message = ''; /* errors for this bot */ g.errors = {}; g.errors.noUsersAllLink = 'Unable to find a@rel="users-all" link'; g.errors.noUserLink = 'Unable to find a@rel="user" link'; g.errors.noRegisterLink = 'Unable to find a@rel="register" link'; g.errors.noMessagePostLink = 'Unable to find a@rel="message-post" link'; g.errors.noRegisterForm = 'Unable to find form@class="add-user" form'; g.errors.noMessagePostForm = 'Unable to find form@class="message-post" form'; g.errors.registerFormError = 'Unable to fill out the form@class="add-user" form'; g.errors.messageFormError = 'Unable to fill out the form@class="message-post" form'; /* some aesop's quotes to post */ g.quotes = []; g.quotes[0] = 'Gratitude is the sign of noble souls'; g.quotes[1] = 'Appearances are deceptive'; g.quotes[2] = 'One good turn deserves another'; g.quotes[3] = 'It is best to prepare for the days of necessity'; g.quotes[4] = 'A willful beast must go his own way'; g.quotes[5] = 'He that finds discontentment in one place is not likely to find happiness in another'; g.quotes[6] = 'A man is known by the company he keeps'; g.quotes[7] = 'In quarreling about the shadow we often lose the substance'; g.quotes[8] = 'They are not wise who give to themselves the credit due to others'; g.quotes[9] = 'Even a fool is wise-when it is too late!';
Making requests
When the page first loads, a set of state variables are populated based on data in the query string. This data is then used to fire off a request to the microblogging server:
function init() { g.status = getArg('status')||'start'; g.url = getArg('url')||g.startUrl; g.body = getArg('body')||''; g.idx = getArg('idx')||0; updateUI(); makeRequest(); } function newQuote() { g.idx++; nextStep('start'); } function updateUI() { var elm; elm = document.getElementById('status'); if(elm) { elm.innerHTML = g.status + '<br />' + g.url + '<br />' + unescape(g.body); } } function makeRequest() { var ajax, data, method; ajax=new XMLHttpRequest(); if(ajax) { ajax.onreadystatechange = function() { if(ajax.readyState==4 || ajax.readyState=='complete') { processResponse(ajax); } }; if(g.body!=='') { data = g.body; method = 'post'; } else { method = 'get'; } ajax.open(method,g.url,true); if(data) { ajax.setRequestHeader('content-type','application/x-www-form-urlencoded'); ajax.setRequestHeader('authorization','Basic '+Base64.encode(g.user.user+':'+g.user.password)); } g.url=''; g.body=''; ajax.setRequestHeader('accept','application/xhtml+xml'); ajax.send(data); } }
Processing responses
Processing responses is the heart of this example application.
Each response representation from the server can potentially contain
links and/or forms of interest for this bot. The code needs to know
what is expected in this response (e.g. “There should be a
‘register’ link in this response somewhere...”) and know how to find
it (e.g. “Give me all of the links in this response and see if one
is marked rel='register'
”). This first code snippet
shows the routine used to route response representations to the
proper function for processing:
/* these are the things this bot can do */ function processResponse(ajax) { var doc = ajax.responseXML; if(ajax.status===200) { switch(g.status) { case 'start': findUsersAllLink(doc); break; case 'get-users-all': findMyUserName(doc); break; case 'get-register-link': findRegisterLink(doc); break; case 'get-register-form': findRegisterForm(doc); break; case 'post-user': postUser(doc); break; case 'get-message-post-link': findMessagePostForm(doc); break; case 'post-message': postMessage(doc); break; case 'completed': handleCompleted(doc); break; default: alert('unknown status: ['+g.status+']'); return; } } else { alert(ajax.status) } }
The following function looks for a link and responds accordingly:
function findMyUserName(doc) { var coll, url, href, found; found=false; url=g.startUrl; coll = getElementsByRelType('user', 'a', doc); if(coll.length===0) { alert(g.errors.noUserLink); } else { for(i=0,x=coll.length;i<x;i++) { if(coll[i].firstChild.nodeValue===g.user.user) { found=true; break; } } if(found===true) { g.status = 'get-message-post-link'; } else { g.status = 'get-register-link'; } nextStep(g.status,url); } }
This bot is also able to locate a form and fill it in based on data already in memory:
function findRegisterForm(doc) { var coll, url, msg, found, i, x, args, c, body; c=0; args = []; found=false; elm = getElementsByClassName('user-add','form',doc)[0]; if(elm) { found=true; } else { alert(g.errors.noRegisterForm); return; } if(found===true) { url = elm.getAttribute('action'); coll = elm.getElementsByTagName('input'); for(i=0,x=coll.length;i<x;i++) { name = coll[i].getAttribute('name'); if(g.user[name]!==undefined) { args[c++] = {'name':name,'value':g.user[name]}; } } coll = elm.getElementsByTagName('textarea'); for(i=0,x=coll.length;i<x;i++) { name = coll[i].getAttribute('name'); if(g.user[name]!==undefined) { args[c++] = {'name':name,'value':g.user[name]}; } } } if(args.length!=0) { body = ''; for(i=0,x=args.length;i<x;i++) { if(i!==0) { body +='&' } body += args[i].name+'='+encodeURIComponent(args[i].value); } alert(body); nextStep('post-user',url,body); } else { alert(g.errors.registerFormError); } }
There are a number of other processing routines in the working example. See for Source Code details on how to locate and download the full source for this book.
Support routines
This implementation contains a few routines used to support the high-level processing outlined in previous sections. For example, below is a function to parse the query string of the current document and a function used to assemble the URI for the next request in the task chain:
function getArg(name) { var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search); return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); } function nextStep(status,url,body) { var href,adr; href = window.location.href; href = href.substring(0,href.indexOf('?')); adr = href + '?status=' + status; adr += '&idx=' + g.idx; if(url) {adr += '&url=' + encodeURIComponent(url);} if(body) {adr += '&body=' + encodeURIComponent(body);} window.location.href = adr; }
This last routine handles parsing a response representation to
find elements that match a specific link-relation value. This is
very similar to looking for elements with specific values in the
class
attribute:
function getElementsByRelType(relType, tag, elm) { var testClass = new RegExp("(^|\\s)" + relType + "(\\s|$)"); var tag = tag || "*"; var elements = (tag == "*" && elm.all)? elm.all : elm.getElementsByTagName(tag); var returnElements = []; var current; var length = elements.length; for(var i=0; i<length; i++){ current = elements[i]; if(testClass.test(current.getAttribute('rel'))){ returnElements.push(current); } } return returnElements; }
Summary
This chapter covered the topic of using HTML5 as a base media type. This has the advantages of a fully-functional hypermedia type (supports almost all the H-Factors identified in Identifying Hypermedia : H-Factors). Using HTML5 also means that implementing the basic server will result in a working client implementation that can run in common web browsers without the need for client-side scripting. This is a great way to build quick sample implementations to test server transition details and media type design aspects.
Since HTML5 is a domain-agnostic media type, it has no built-in
domain specific markup elements and no predefined transitions as does
Atom/AtomPub and the Collection+JSON example in Chapter 3. This example showed how designers can use
HTML5 attributes (id
, name
, class
,
and rel
) to apply domain-specific details to
responses.
A server implementation of a simple CouchDB data model was created that included support for HTTP Basic Authentication. This showed that user authentication details can be implemented independently of the actual media type design. And finally, two sample clients were reviewed. The first was simply a CSS restyling of the plain HTML5 rendered by the server (the POSH client). The second client was an Ajax-style web bot implementation that parsed the HTML5 responses looking for desired links and forms in order to post messages to the server.
This implementation showed that it is possible to use an already-existing hypermedia type as the basis for your own unique design. It also showed the importance of documenting domain-specific details in ways that both server and client implementors can understand. The next (and final) chapter explores the role of documentation in more detail.
Get Building Hypermedia APIs with HTML5 and Node 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.