Creating an Embeddable Medium Posts Widget Using React — Part 1.

Github Page Articles Section With Embedly

Introduction

So I had to continue looking for other options. While on my search I discovered two blog posts by Chris John of Retainable and Dana Janoskova, where they both described the creation of something similar. They were not particularly fitting for my use case — vue-rss-blog by Chris, retrieves all the medium posts while the Vuidget by Dana would require an extra learning curve — but they gave me an idea on how to create mine.

I recently started exploring with fetching APIs using the react library. When I am trying to learn a new thing I like to get my hands dirty after following one or two tutorials and so far I was able to work on creating this photo and articles finder and the table part of this meteorite landings explorer. So it made logical sense for me to try to create this widget using React and that is what I did.

Caveat Emptor: I am still very much of a noob at React and this is one of my attempts to work with the library to create something useful and document it so I am open to suggestions if you find something that is off in what I did.

Getting medium user data

The feed however is in XML and must be converted to JSON before it can be of any use to me. There’s a really cool service I found a while ago that converts RSS to JSON called RSS to JSON Converter Online and it has allows you do conversions even without an account. I also found this cool article that talked about how to display medium articles on a site on using this tool with the fetch API in vanilla JS. The execution of the idea was all falling into place.

Setting up my project

Creating my Widget Component

import React, {Component} from 'react';
import './Widget.css';
class Widget extends Component {
constructor(props) {
super(props)
this.state = {
requestFailed: false,
active: 0
}
}
render (){
return(
<div className="container">
</div>
)
}
}
export default Widget;

The component has two states:

  1. requestFailed to check if the request was failed. It is set to false.
  2. active which is used for the carousel items. It is set to 0 so that it matches the index of the first carousel item created later.

The render function will return the container that will hold the carousel for the slide. The props will be the medium feed url that will be passed to the Widget Component when it is rendered in index.js.

Rendering the Widget Component.

import React from 'react';
import ReactDOM from 'react-dom';
import Widget from './Widget';
const el = document.getElementById('medium-posts-widget');
const rssFeedLink = el.getAttribute('data-medium-rss')
ReactDOM.render(<Widget mediumRSSFeedLink={rssFeedLink} />, el);

The el is the div element in the public/index.html folder. I decided to give mine an id of medium-posts-widget instead of root and gave it a data attribute of data-medium-rss. The data-medium-rss attribute is where I passed in the url to my medium feed. This is what the public/index.html file looked like at this point — note the item highlighted in bold.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="A Website Created for A Widget for Embedding Medium Posts"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css?family=Playfair+Display:400,900&amp;display=swap" rel="stylesheet">
<title>Medium Posts Widget</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="medium-posts-widget" data-medium-rss="https://medium.com/feed/@adamichelllle"></div>
<script src="https://code.jquery.com/jquery-3.4.1.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>

Fetching Data from User Medium Feed

const urlForFeedToJson = feed => `https://api.rss2json.com/v1/api.json?rss_url=${feed}`;
const keyCategories = ['javascript', 'front-end-development', 'responsive-web-design'];
const contains = (keyCategories, categories) => {
const [category1, category2, category3] = keyCategories
return categories.indexOf(category1) > -1 || categories.indexOf(category2) > -1 || categories.indexOf(category3) > -1
}

Explanation: urlForFeedToJSON is a function that takes in a feed parameter i.e the feed url and returns a string of the RSStoJson API endpoint I want to fetch. keyCategories is an array of the categories I want to filter by. contains is a function that checks if any of the elements in keyCategories can be found in the categories array gotten from the feed JSON.

As explained by several tutorials online and even in the react guide, if you’re going to be making an API call in your react component, you should do it in the componentDidMount() function. Therefore, just after the constructor function, I made an API call to RSS to JSON’s API to retrieve my feed like this:

componentDidMount() {
fetch(urlForFeedToJson(this.props.mediumRSSFeedLink))
.then(response => {
if (!response.ok) {
throw Error("Network request failed")
}
return response
})
.then(data => data.json())
.then(data => {
const dataItems = data.items
const mediumPosts = dataItems.filter(item => item.categories.length > 0)
this.setState({
mediumPosts: mediumPosts
})
}, () => {
this.setState({
requestFailed: true
})
})
}

The above code fetches the API, returns the JSON data, retrieves items i.e posts from the JSON data, filters them to ensure they have categories, stores them in the const mediumPosts and then sets a new state called mediumPosts with the const mediumPosts. If the request fails, i.e if I don’t get a true value for response.ok, I throw a new Error. This error will be caught in the anonymous function found in the second .then chain. I set the state requestFailed to true.

Filtering Feed JSON by Specific Array of Categories

const mediumPosts = dataItems.filter(item => item.categories.length > 0 && contains(keyCategories, item.categories))

Now I have my posts stored in the state mediumPosts. This is what I used each time I wanted to retrieve my posts as you’ll see.

Adding the carousel for the slides

        <div id="postsCardCarousels" className="carousel slide" data-ride="carousel" data-interval="false">
<div className="carousel-inner row w-100 mx-auto" role="listbox">

</div>
<a className="carousel-control-prev" href="#postsCardCarousels" role="button" data-slide="prev">
<span className="carousel-control-prev-icon" aria-hidden="true"></span>
<span className="sr-only">Previous</span>
</a>
<a className="carousel-control-next" href="#postsCardCarousels" role="button" data-slide="next">
<span className="carousel-control-next-icon" aria-hidden="true"></span>
<span className="sr-only">Next</span>
</a>
</div>

I also added the styles for the carousel in the Widget.css file.

Creating the Card Component

import React from 'react';const tagToText = (node) => {
let tag = document.createElement('div')
tag.innerHTML = node
node = tag.innerText
return node
}
const shortenText = (text,startingPoint ,maxLength) => {
return text.length > maxLength?
text.slice(startingPoint, maxLength):
text
}
function Card(props) {
return(
<div className="card">
<img src={props.post.thumbnail} className="card-img-top post-thumbnail" alt={props.post.title}></img>
<div className="card-body">
<h5 className="card-title post-title">{props.post.title}</h5>
<p className="card-text post-preview">{'...' + shortenText(tagToText(props.post.content), 60, 200) + '...'}</p>
<a href={props.post.link} className="btn btn-link-grey">Read this article on Medium.com</a>
</div>
</div>
)
}
export default Card

Importing and Using the Card Component to create Carousel Items

import Card from './components/Card';

In my render function, just above the return statement I added the following lines of code:

    if (this.state.requestFailed) return <div className="container"><p className="ml-4">Oops! Sorry we couldn't load your medium articles</p></div>
if (!this.state.mediumPosts) return <div className="container"><p className="ml-4">Loading articles from my medium feed ...</p></div>

const mediumPosts = this.state.mediumPosts
const posts = mediumPosts.length > 6 ? mediumPosts.slice(7) : mediumPosts;

const cardCarousels = posts.map((post, index) =>
<div className={this.state.active === index ? 'carousel-item col-12 col-sm-12 col-md-6 col-lg-4 active' : 'carousel-item col-12 col-sm-12 col-md-6 col-lg-4'} key={index}>
<Card post={post} />
</div>
)

The first if statement checks if the this.state.requestFailed state is set to true. If it is, I return a div with the ‘Oops! Sorry we couldn’t load your medium articles’ message. The second if checks if the this.state.mediumPosts has not been set. If it is true i.e if this.state.mediumPosts has not yet been set, I return a div with the ‘Loading articles from my medium feed …’ message which shows briefly while the fetch is happening.

The next line gets the all the posts from this.state.mediumPosts and stores it in mediumPosts. The one after it returns a slice of the first 6 elements if the length of mediumPosts is greater than 6 or the whole mediumPosts array if it is less and stores it in posts.

In the last part, I mapped each element of the posts array, taking both the post and the index. For each post, I created a carousel-itemdiv element with a className attribute containing a tenary expression that checks if the index of each post matches this.state.active then returns a string of classes containing the word ‘active’ and if it does not it returns a string without the word ‘active’. This is vital because it ensures that only the first carousel item contains the ‘active’ class which is important for the slider to work. Also in that carousel-item div element, I add the card component imported earlier.

Finally, in the return statement found the render function, within the div with the class carousel-inner, I added the const cardCarousels.

When I run npm start in my terminal from that folder, the result is this

Picture of what the widget looks like. — I only have one post in the categories I chose which is why there’s only one post.

My widget was done. I have to create a build of it so that I can host the static JavaScript and CSS files and use them wherever I want. In the next article, I will discuss how I was able to do that.

Thanks for reading. Feel free to share if you enjoyed reading this. Till then.

Libraries and Tools

  1. RSS to JSON Online Converter.

Resources

  1. How to embed Medium on your website the easy way by Chris John.
  2. How to create an embeddable Vue Widget with Custom Vue Component by DanaJanoskova.
  3. How to Display Medium Posts on a Website with Plain Vanilla JS Basic API Usag by Konrad Dariusz Wołoszyn.
  4. Bootstrap Carousel Multiple Items by Anil.
  5. Display Medium articles on your site.

Part 2 of this article can be found here: Creating a Medium Posts Widget Using React — Part 2.

Front End Dev || Writer || Blogger