How I built a "top stories" feed in vanilla JS

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.

Today’s top stories

The HTML

Before we do anything else, let’s look at the HTML I started with.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="Today's top stories from The New York Times.">
    <title>Today's Top Stories from The New York Times</title>
    <link href="assets/css/styles.css" rel="stylesheet">
  </head>
  <body>
    <h1>Top Stories</h1>
    <div id="app">
      <p>
        <a href="https://www.nytimes.com/trending/">Today's top stories from <i>The New York Times</i></a>
      </p>
    </div>
    <script crossorigin="anonymous" src="https://polyfill.io/v3/polyfill.min.js?features=default%2Cfetch"></script>
    <script src="assets/js/app.js"></script>
  </body>
</html>

Instead of leaving the div#app element empty, I added a paragraph with a link to today’s top stories on the New York Times website.

This is important because if the script fails to work for whatever reason (e.g. a bad network connection), the user will still get something useful. We call this principle progressive enhancement.

I also added the default bundle of polyfills from polyfill.io, along with a polyfill for the Fetch API. This allows me to use modern JavaScript features while still supporting older browsers.

The CSS

I added some very basic styles to make my project look a bit nicer.

* {
  box-sizing: border-box;
}

body {
  width: 88%;
  max-width: 30em;
  margin: 1em auto;
  line-height: 1.5;
}

The universal (*) rule makes the box model a bit easier to work with. The body rule centers the page and increases its line height for better readability.

The JavaScript

As always, I started with an immediately-invoked function expression (IIFE). This keeps my code outside the global scope and allows me to refer to the document as d for short. I also opted into strict mode.

;(function (d) {
  "use strict";
})(document);

Variables

I then created my variables.

var endpoint = "https://api.nytimes.com/svc/topstories/v2/home.json?api-key=APIKEY";
var app = d.querySelector("#app");

I saved the API endpoint to a variable so I can easily change it later without having to find it in multiple places. I also got a reference to the #app element.

Functions

Next, I created a series of functions to pass into the then() and catch() methods. But first, I added a helper function to sanitize the third-party data.

sanitizeHTML()
/**
 * 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
 * @return {String} str  The sanitized string
 */
function sanitizeHTML (str) {
  var temp = document.createElement("div");
  temp.textContent = str;
  return temp.innerHTML;
}

This is important for preventing XSS attacks.

getJSON()

The first function I created gets the JSON data from a Fetch request.

/**
 * Get the JSON data from a Fetch request
 * @param   {Object} response The response to the Fetch request
 * @returns {Object}          The JSON data OR a rejected Promise
 */
function getJSON (response) {
  return response.ok ? response.json() : Promise.reject(response);
}

If the request is successful (i.e. its ok property is true), I call the json() method on the response.

Otherwise, I need to create a rejected promise. I do this by calling the static Promise.reject() method, passing in the response.

getStories()

Once the JSON comes back, I need to get the actual data. The New York Times API stores it in a results property, so I created a function to return that.

/**
 * Get the stories from the JSON data
 * @param   {Object} data The JSON data
 * @returns {Array}       The stories
 */
function getStories (data) {
  return data.results;
}

I could have renamed my getJSON() function to getData(), and instead returned response.json().results directly. This would have eliminated one call to the then() method. I like the separation of concerns between getting the JSON and the actual results, though.

buildListItem()

After the call to the getStories() function, I’m left with an array of stories. Each item in the array is an object containing information about that story. I created a function, buildListItem(), to create a list item for each one.

/**
 * Create a list item for a story
 * @param   {Object} story The story
 * @returns {String}       An HTML string for the story
 */
function buildListItem (story) {
  return (
    "<li>" +
      "<article>" +
        "<h2>" +
          "<a href='" + sanitizeHTML(story.url) + "'>" +
            sanitizeHTML(story.title) +
          "</a>" +
        "</h2>" +
        "<p>" +
          sanitizeHTML(story.byline) +
        "</p>" +
        "<p>" +
          sanitizeHTML(story.abstract) +
        "</p>" +
      "</article>" +
    "</li>"
  );
}

Notice that I’m passing the third-party data to the sanitizeHTML() function to prevent XSS attacks.

This function only works on a single array item, though, so I’ll pass it into the Array.prototype.map() method next.

insertHTML()

This function replaces the #app element’s contents with the data from the API.

/**
 * Insert the API data into the DOM
 * @param {Array} stories The array of stories
 */
function insertHTML (stories) {
  app.innerHTML =
    "<ul>" +
      stories.map(buildListItem).join("") +
    "</ul>";
}

I first set the #app element’s innerHTML property to an opening <ul> tag.

I then pass my buildListItem() function into the map() method before calling the Array.prototype.join() method. This converts each object in the array to an HTML string.

I add this to the innerHTML property, along with the closing </ul> tag. The result is a complete unordered list element.

insertError()

Finally, I created a function to handle any errors in my chain of promises. I’ll pass this into the catch() method.

/**
 * Insert an error message into the DOM
 */
function insertError () {
  app.innerHTML =
    "<p>Sorry, there was a problem getting today's stories!</p>" +
    "<p>Please try again later.</p>";
}

This just replaces the #app element’s contents with a generic error message.

Initialize the app

All that’s left to do now is initialize the app! I called the global fetch() method, adding a chain of promises and passing in each of my functions.

fetch(endpoint)
  .then(getJSON)
  .then(getStories)
  .then(insertHTML)
  .catch(insertError);

It’s so good to use named functions like this because you can literally just read what’s happening, line by line, in plain English.

Multiple categories

My work thus far shows the top stories for the day, but it would be nice if it could show the top stories from a few different categories. I’d like to know what’s happening in movies, science, and technology.

Update the variables

First up, I created a new categories variable at the top of my script. It’s just an array containing each of my desired categories as strings.

var endpoint = "https://api.nytimes.com/svc/topstories/v2/";
var apiKey = "APIKEY";
var categories = [ "movies", "science", "technology" ];
var app = d.querySelector("#app");

I also split my endpoint and API key into two separate variables. You’ll soon see why this is necessary.

Change the functions

The biggest change is the logic of my functions. Instead of making a single request, I need to make multiple, passing in the desired category each time.

buildArticle()

I renamed my buildListItem() function to buildArticle(). It’s mostly the same, but I tweaked the HTML string a bit.

/**
 * Build the HTML for an article
 * @param  {Object} story The object representing the current article
 * @return {String}       An HTML string for the article
 */
function buildArticle (story) {
  return (
    "<article>" +
      "<header>" +
        "<h3>" +
          "<a href='" + sanitizeHTML(story.url) + "'>" +
            sanitizeHTML(story.title) +
          "</a>" +
        "</h3>" +
      "</header>" +
      "<address>" +
        "<p>" +
          sanitizeHTML(story.byline) +
        "</p>" +
      "</address>" +
      "<p>" +
        sanitizeHTML(story.abstract) +
      "</p>" +
    "</article>"
  );
}

I changed the title from an <h2> to an <h3>, because the <h2> elements will now need to be the category names. I also added some more semantic HTML elements, like <header> for the title, and <address> for the byline.

render()

Next up, I created a function to render all articles of a specific category.

/**
 * Insert the HTML for all articles of a specific category into the DOM
 * @param {Array}  articles An array of objects for each article
 * @param {String} category The category to use
 */
function render (articles, category) {
  app.innerHTML +=
    "<section>" +
      "<header>" +
        "<h2>" + category + "</h2>" +
      "</header>" +
      articles.slice(0, 3).map(buildArticle).join("") +
    "</section>";
}

I add a <section> for each category—more semantic HTML, which is good. I also add a second-level heading for the category name. I then use the Array.prototype.slice() method to get the first three articles, and I turn them into an HTML string just like I did originally.

fetchArticles()

I moved my fetch() invocation into a new function called fetchArticles(). I did this so I can call it once for each category.

/**
 * Fetch the articles for a category and insert them into the DOM
 * @param {String} category The category to use
  */
function fetchArticles(category) {
  fetch(endpoint + category + ".json?api-key=" + apiKey)
    .then(getJSON)
    .then(function(data) {
      render(data.results, category);
    })
    .catch(insertError);
}

This is why I needed to separate the endpoint and API key into their own variables. The endpoint is now dynamic, so I need to append the category each time this function is invoked.

Initialize the app (again)

Now all I need to do is initialize the app.

// Remove the default link to The New York Times
app.innerHTML = "";

// Insert three articles for each category
categories.forEach(fetchArticles);

I first empty the #app element, which is necessary because my new render() function only appends strings to the innerHTML property. It never resets it entirely. This is because it gets called more than once; resetting the property each time would mean only the final category would be shown.

Finally, I use the Array.prototype.forEach() method to call my fetchArticles() function once for each category.

Wrapping up

And there you have it: a really clean, minimal app that gets the top stories for the day! I love it because it’s so much less intrusive than other news aggregators. No bullshit ads or tracking, just the content.

You can view the demo and check out the full source code on GitHub.

If you want to learn how to build cool apps like this, I highly recommend you join the next session of The Vanilla JS Academy. It starts on May 11th. Chris is such a good guy and he will look after you! ❤️