My fourth challenge for the Vanilla JS Academy was to build a news feed using the Top Stories API from The New York Times. Let's look at how I tackled it.

Before we do anything else, let's look at the HTML I started with:

<header>
<h1>NYT Top Stories</h1>
<p>Here are <a href="https://www.nytimes.com/trending/">today's top stories</a> from <i>The New York Times</i>.</p>
</header>

<main>
<p>
<strong>Getting today's top stories...</strong>
</p>
</main>

I have a link to today's top stories on the New York Times website. I put this there in case the JavaScript fails to load so that the user can still access the content. I also have a main element into which I will inject the stories.

Getting today's top stories

First, I created two variables: one for the main element, and one for the API endpoint.

I used Cloudflare Workers to hide my API key, which is why my endpoint is different from the one in the Top Stories API documentation:

// Get the element that will contain the stories
var main = document.querySelector("main");

// Store the endpoint
var endpoint = "https://nyt.kbarker.workers.dev";

Then I added in my getJSON() helper function...

/**
* Get the JSON data from a Fetch request
* @param {Object} response The response to the request
* @returns {Object} The JSON data OR a rejected promise
*/

function getJSON (response) {
return response.ok ? response.json() : Promise.reject(response);
}

...And made a Fetch request to my endpoint. I used my getJSON() function to get the data, and the console.log() method to log it to the console. I also used the console.error() method to log any errors:

fetch(endpoint)
.then(getJSON)
.then(console.log)
.catch(console.error);

The returned object contains a results property. It's an array of objects that represent today's top stories.

I created a function that builds an HTML string for each of these objects:

/**
* Build the HTML string for a single story
* @param {Object} story The object returned by the API
* @returns {String} An HTML string
*/

function buildStory (story) {

return (
"<article>" +
"<header>" +
"<h2>" +
"<a href='" + story.url + "'>" + story.title + "</a>" +
"</h2>" +
"<p>" +
"<b>Last updated: </b>" +
"<time datetime='" + story.updated_date + "'>" +
new Date(story.updated_date).toLocaleString() +
"</time>" +
"</p>" +
"</header>" +
"<p>" + story.abstract + "</p>" +
"</article>"
);

}

Then I created another function that:

  1. Gets the first five story objects (data.results.slice(0, 5))
  2. Converts them into HTML strings (.map(buildStory))
  3. Joins the whole array into one string (.join(""))
  4. Adds the final HTML string to the page (main.innerHTML =)
/**
* Add the first five stories to the DOM
* @param {Array} data The array of objects returned by the API
*/

function showStories (data) {
main.innerHTML = data.results.slice(0, 5).map(buildStory).join("");
}

I also added a function to show an error message if necessary. This is a better user experience than just logging it to the console:

/**
* Add an error message to the DOM
*/

function showError () {

main.innerHTML = (
"<p>" +
"<strong>Sorry, there seems to be a problem. You can still view today's top stories using the link above.</strong>" +
"</p>"
);

}

Then I wrapped the entire promise chain inside a function, for the sake of readability:

/**
* Fetch the top five stories and add them the DOM
*/

function getStories () {

fetch(endpoint)
.then(getJSON)
.then(showStories)
.catch(showError);

}

And finally, called it to initialize the script:

// Fetch the top five stories and add them the DOM
getStories();

View demo on CodePen

Multiple categories

I wanted to be able to show stories from multiple categories. To begin, I created a new variable to store an array of categories:

// The categories to fetch
var categories = ["food", "movies", "technology"];

I also modified my buildStory() function. All I did was change the h2 to an h3, since the categories will now be the second-level headings:

"<h3>" +
"<a href='" + story.url + "'>" + story.title + "</a>" +
"</h3>"

Then I created a function to build the HTML string for a single category. It creates an h2 for the category name itself, and then an article for each of the first three stories under that category:

/**
* Build the HTML string for a single category
* @param {Array} stories An array of story objects
* @param {String} category The category to use
* @returns {String} An HTML string
*/

function buildCategory (stories, category) {

return (
"<h2>" + category + "</h2>" +
stories.slice(0, 3).map(buildStory).join("")
);

}

I passed this into another function, fetchCategory(), to return a Fetch request for a given category in my categories array. This time, I made a POST request and passed the category to my endpoint as a JSON string:

/**
* Return a Fetch request for the given category
* @param {String} category The category to use
* @returns {Object} A promise object
*/

function fetchCategory (category) {

// Options for the request
var options = {
method: "POST",
body: JSON.stringify({ category: category })
};

// Return a promise for the request
return (
fetch(endpoint, options)
.then(getJSON)
.then(function (data) {
return buildCategory(data.results, category);
})
);

}

Next, I modified my showStories() function to showCategories(). It will now take the final HTML string—of all categories and articles—and add it to the DOM:

/**
* Join the category strings and add them to the DOM
* @param {Array} categories The array of category strings
*/

function showCategories (categories) {
main.innerHTML = categories.join("");
}

My showError() function is the same as before.

Finally, I modified my getStories() function to:

  1. Get an array of Fetch requests for the categories
  2. Create a single promise that resolves to an array of the results of the Fetch requests
  3. Join the resolved array and add it to the DOM
/**
* Add all stories from all categories to the DOM
*/

function getStories () {

// Create an array of Fetch requests for the categories
var requests = categories.map(fetchCategory);

// This will resolve once ALL the requests have resolved
var categoryStrings = Promise.all(requests);

// Join the resolved array and add it to the DOM
categoryStrings.then(showCategories).catch(showError);

}

Again, I initialized the script by calling this function:

// Add all stories from all categories to the DOM
getStories();

View demo on CodePen

Sanitizing the third-party data

The script works, but there's a big security flaw. We still need to sanitize the third-party data to prevent XSS attacks.

Thankfully, this is easy to do. We just need to add in a helper function:

/**
* Sanitize and encode all HTML in a user-submitted string
* (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {String} str The user-submitted string
* @returns {String} The sanitized string
*/

function sanitizeHTML (str) {
var temp = document.createElement("div");
temp.textContent = str;
return temp.innerHTML;
}

And then wrap each piece of third-party data inside a call to this function:

/**
* Build the HTML string for a single story
* @param {Object} story The object returned by the API
* @returns {String} An HTML string
*/

function buildStory (story) {

// Sanitize the date here for readability
var date = sanitizeHTML(story.updated_date);

// Return the HTML string
return (
"<article>" +
"<header>" +
"<h3>" +
"<a href='" + sanitizeHTML(story.url) + "'>" + sanitizeHTML(story.title) + "</a>" +
"</h3>" +
"<p>" +
"<b>Last updated: </b>" +
"<time datetime='" + date + "'>" +
new Date(date).toLocaleString() +
"</time>" +
"</p>" +
"</header>" +
"<p>" + sanitizeHTML(story.abstract) + "</p>" +
"</article>"
);

}

View demo on CodePen

If you want to build cool projects like this, I strongly encourage you to sign up for the next session of the Vanilla JS Academy! ❤️