Creating an Embeddable Medium Posts Widget Using React — Part 1.
Introduction
I was recently working on revamping my user Github page and I was looking for a way to embed some of my web development and JavaScript projects medium posts on it. I initially settled for creating an embedly embed using their website and including the generated code in my html file but I didn’t really like how it turned out especially as it came with watermarks since it was a free embed (see image above).
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
As at the time of writing this, Medium’s API is write only and therefore has no provision for reading user data so it was not going to be of any use to me. They however have an RSS feed for each user and publication which looks like this: https://medium.com/feed/@{your_username}. This feed contains the posts along with their categories i.e those tags medium asks you for at the when you want to publish. These categories are what I am going to use to filter the list of posts I am eventually going to get.
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
I created a react app using create-react-app
and named it medium-posts-widget
. In my src
folder, I renamed my App.js
, App.css
to Widget.js
and Widget.css
respectively, removed the service-worker.js
andlogo.svg
files, then in my index.js
file, I changed the import and render calls to reflect my new files. Also in my public/index.html
folder, I linked the relevant CDNs I would be using like the Bootstrap Style Sheet, Google’s PlayFair font, JQuery and Bootstrap.js. Next, I removed the styles in Widget.css
since I was going to replace them with mine. Lastly, I removed the header and .app elements from my Widget.js
file and replaced them with a single div
element.
Creating my Widget Component
Recall that the App.js
now called Widget.js
component was a function component. I wanted to be able to store states of the widget so I changed it to a class component and added the following code.
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:
requestFailed
to check if the request was failed. It is set to false.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.
The widget component will now be rendered in index.js
like this:
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&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
Before doing this I had to declare some variable and functions at the top of my file just after the import statements.
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
Since I wanted to just return posts I have written about front-end-development, JavaScript and responsive-web-design, I had to use the contains
function in the code above. Where I had const mediumPosts = dataItems.filter(item => item.categories.length > 0)
, I added && contains(keyCategories, item.categories)
, such that the line would became:
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
I wanted my widget to make use of cards like this Bootstrap 4 carousel with 3 card items, I once customize for my personal use case which I originally found here made by Anli from AzMind (thank you 😃). Therefore I had to add that bootstrap carousel component. I added the code found below within the div
having the class container
. The .caousel-inner
div will contain the carousel items which I created after creating the card component.
<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
I created a Card.js
component in a components
folder which was going to take in a post
props. This component was going to create a bootstrap card with each post details. I wanted to pick only the post-title, post-content, link to the post and finally the thumbnail. The post content however is too long to be fitted into a card and I wanted to truncate it and create like a preview of the post. From the article above I was able to get a function to convert the post-content to text and shorten the text. So at the end of the day my card component looked like this:
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
At the top of my Widget.js
file after the last import statement, I added another import for my card component.
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
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
- Create React App
- RSS to JSON Online Converter.
Resources
I’m really grateful for all these resources listed below that helped me in doing this.
- How to embed Medium on your website the easy way by Chris John.
- How to create an embeddable Vue Widget with Custom Vue Component by DanaJanoskova.
- How to Display Medium Posts on a Website with Plain Vanilla JS Basic API Usag by Konrad Dariusz Wołoszyn.
- Bootstrap Carousel Multiple Items by Anil.
- Display Medium articles on your site.
Part 2 of this article can be found here: Creating a Medium Posts Widget Using React — Part 2.