paint-brush
Building a Blog with React and Contentfulby@aaron.klaser
27,660 reads
27,660 reads

Building a Blog with React and Contentful

by Aaron KlaserJanuary 30th, 2018
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This article is part two in a weekly series on building a blog with React and is an extension of the code created in part one.

Company Mentioned

Mention Thumbnail
featured image - Building a Blog with React and Contentful
Aaron Klaser HackerNoon profile picture

The React Blog Series: Part Two

This article is part two in a weekly series on building a blog with React and is an extension of the code created in part one.

The React Blog Series






Part One: Building a website with React and BulmaPart Two: Building a Blog with React and ContentfulPart Three: Import your Medium Feed into ReactPart Four: Adding a Redux to a React BlogPart Five: Replacing Redux Thunks with Redux SagasPart Six: (In Progress) Writing Unit Test for a React Blog with Redux Sagas

This will go over the basics of setting up Contentful and displaying its data as a blog feed in our React application.

This does not include Redux… yet ;)

Contentful

Sign up for Contentful

Click here to Sign Up Here

When you start poking around Contentful, you might get a strange failure feeling, daja vue perhaps, or is it just that it looks exactly like the Wordpress Admin?

Contentful is very clean, easy to navigate, and it looks and feels a lot like Wordpress. As much as hate to admit, I love it and thats an amazing thing because I loth Wordpress. To be fair, I don’t hate Wordpress for its UX, I hate it because its slow and built on what looks like hacked together PHP, and lets face it, no one actually likes PHP. Not even the creator of PHP.

The Concept

Contentful is content management as a service (CaaS), this replaces the need to have to connect your site to that terrible MySQL database hosted on some crappy Apachy Server, where you have to install phpMyAdmin and spend hours trying to figure out cPanel.

The Data

There are two parts to your data, the structure and the data. It’s basically a really nice web form the converts to simple json. You have to first set up your form the in Content Model, then you can hop over to Content and fill out your forms, each form becomes an item in the models array of data sent via rest end points.

Let start by making a Blog Model

Add in three fields: Title, Path, and Content

The JSON preview will look like this

Now let’s create a Blog post for the app to pull and display on our Blog page.

Go to Content, and Click Add Blog in the top corner and fill out the form.

Now you have your first blog post!

Get Api Token

You will need a Space Id and access token to retrieve your data. Go to the APIs tab and click Add Api Key.

Name it what ever you want and save.

Set Up Contentful in React

npm install contentful

Let’s just see if we can connect and get our data.

On src/index.js let’s import Contentful, set up a simple client and then getEntries. Now let’s console log what we get, with any luck we should be see our test blog post. This is temporary.







import React from 'react';import ReactDOM from 'react-dom'import registerServiceWorker from './registerServiceWorker'import { BrowserRouter as Router } from 'react-router-dom'import * as contentful from 'contentful'import App from './App'import './index.css'



var client = contentful.createClient({space: 'qu10m4oq2u62',accessToken: 'f4a9f68de290d53552b107eb503f3a073bc4c632f5bdd50efacc61498a0c592a' })







client.getEntries().then(entries => {entries.items.forEach(entry => {if(entry.fields) {console.log(entry.fields)}})})






ReactDOM.render((<Router><App /></Router>), document.getElementById('root'))registerServiceWorker()

Run your app and open your console. You should see a little clean object full our data.

Isn’t that amazing. It was like, the simplest thing to set up ever!

But now let’s do it right… well, as much as we can without Redux :P

Setting Up Your Blog Page

Copy all of your Contentful stuff and delete it from your index.

Head over to your Blog.js and change it from a dumb component to a smart React.Component class.

Then, we will need to include state with an empty post array.

Paste in your Contentful createClient object.

Next, using the React lifecycle hook componentDidMount, we need to call a fetchPosts function which will call a setPosts function


import React from 'react'import * as contentful from 'contentful'

class Blog extends React.Component {



state = {posts: []}




client = contentful.createClient({space: 'qu10m4oq2u62',accessToken: 'f4a9f68de290d53552b107eb503f3a073bc4c632f5bdd50efacc61498a0c592a'})



componentDidMount() {this.fetchPosts().then(this.setPosts);}

fetchPosts = () => this.client.getEntries()





setPosts = response => {this.setState({posts: response.items})}






render() {return (<p>This is the Blog Page</p>)}}

export default Blog

If you plug a console log in the fetchPosts function you will see we are getting our data back from Contentful.

Now that we are getting data and we are doing it the React way (w/o Redux), let’s update the render function to display the data. We will move these to their own pretty component later, but for now let’s stick it in some pre tags.











render() {return (<div><p>This is the Blog Page</p><br/>{ this.state.posts.map(({fields}, i) =><pre key={i}>{JSON.stringify(fields, null, 2)}</pre>)}</div>)}

Yay!!!

Adding New Blog Posts

Head back over to Contentful and click Content > Add Blog, then fill out the information. You know the drill.

Click Publish and head get back over to your app, refresh the browser and BLAMMO!! There’s your second post.

Styling the Blog Feed

We need to set up a Blog Item component. Our Blog page will contain a list of Blog Items which make up “the feed”.

Remember we are using fractal file structures and BlogItem.js will be a child of Blog.js. Since Blog.js is in the app folder, we will create a folder called blog in the app folder, then create our BlogItem.js in our newly created blog folder.

If you’re keeping score, and following Fractal File Structures correctly, our current file structure should look something like this.

Create BlogItem Component

In BlogItem.js, create as simple component with Bulma box. We’ll pass in props which for now will be a spread of the fields from Contentful.

import React from 'react'






const BlogItem = (props) => (<div className="box content"><h1>{props.title}</h1><p>{props.content}</p></div>)

export default BlogItem

Now, back in Blog.js we need update our posts map in render to return our BlogItem component and spread the fields as properties.











render() {return (<div><p>This is the Blog Page</p><br/>{ this.state.posts.map(({fields}, i) =><BlogItem key={i} {...fields} />)}</div>)}

Refresh and there you go. But… that kind of looks terrible.

Style Blog Page

Let’s replace <p>This is the Blog Page</p> with a Bulma Hero for our page header. Since we will probably do this on the top of most of our pages, lets create this as a global reusable component.

In our app folder, create a new folder named components. By our fractal logic this implies that these components will all exist as children of the App.

Spoiler alert: If you haven’t figured it out yet, App.js and it’s folder of children (app), will contain all our presentation logic. For now, every we do will be a child of App. Later when we refactor to use Redux, App will get a sibling named Store.js and its folder of children (store) and it will contain all of our data services and state.

In the components folder create two files, PageHeader.js and PageContent.js

PageHeader will be a component for our Bulma Hero. We will not be including the subtitle content. This way we can do css styling or include links and pass it as a children prop.

import React from 'react'














const PageHeader = (props) => (<section className={`hero ${props.color}`}><div className="hero-body"><div className="container"><h1 className="title">{props.title}</h1><h2 className="subtitle">{props.children}</h2></div></div></section>)

export default PageHeader

And in our Blog.js replace <p>This is the Blog Page</p> with



<PageHeader color="is-info" title="Code Blog">Your standard <strong>JavaScript</strong> programming blog, albeit, probably not very good, but I will at least try to keep it entertaining. This blog is a chronological mix of random posts on Angular, React, Functional Programming, and my <strong>project walkthroughs</strong>.</PageHeader>

PageContent will be a styled component to confine our page to a max width and add padding and what have you.

import styled from 'styled-components'






const PageContent = styled.div`margin: 0 auto;max-width: 960px;padding: 1.5rem;`export default PageContent

Ahh… now that’s much better.

Better More Realistic Blog Posts

Back in Contentful, let’s add some fields to our Blog Content Model

We are going to add three new fields:

  • icon: Set appearance as single line
  • status: Valid as only values “IN_PROGRESS” and not required. Set appearance as Dropdown
  • date: Set appearance as date only

Now hop over to your First and Second blog post and fill in some data.

Icon can be a url link to an icon or base64 as long is it can normally go into a src attribute.

Content should set as Markdown so when you fill your page content it can be styled with Markdown!

Do something like this for both posts, save, then refresh your. It probably going to look terrible…

YUP!

Rendering Markdown

So lets start by addressing the elephant in the room. We need make react render the markdown as html and it needs to look… pretty.

npm install react-markdown

In the BlogItem.js, replace content <p> with the react-markdown.


import React from 'react'import * as Markdown from 'react-markdown'






const BlogItem = (props) => (<div className="box content"><h1>{props.title}</h1><Markdown source={props.content} /></div>)

export default BlogItem

BOOM!

Much better, but I would remove the H1 titles from the Markdown in Contentful and rename your Contentful blog posts with what the H1 title was. You will see why when we add the Icon and stuff in next.

Adding the Icon

This is just simple Bulma. Open your BlogItem.js and we are going to steal the box media example from Bulma’s website.




import React from 'react'import { Link } from 'react-router-dom'import * as Markdown from 'react-markdown'import moment from 'moment'










const BlogItem = (props) => (<div className="box"><article className="media"><div className="media-left"><figure className="image is-64x64"><img src={props.icon} alt="Image" /></figure></div><div className="media-content"><div className="content">

      <h1>{props.title}</h1>  
      <Markdown source={props.content.split(" ").splice(0,150).join(" ").concat('...')} />  
    </div>  
    <div className="level">  
      <div className="level-left">  
        <Link className="level-item button is-small is-link is-outlined" to={props.path}>Read More</Link>  
      </div>  
    <div className="level-right">  
      <p className="level-item has-text-link is-size-7">

        {moment(props.date).calendar(null, {  
          sameDay: '\[Today\]',  
          lastDay: '\[Yesterday\]',  
          lastWeek: '\[Last\] dddd',  
          sameElse: 'MMM Do YYYY'  
        })}  
      </p>  
    </div>  
  </div>  
</div>  



</article></div>)

export default BlogItem

Yeah, so thats it but it’s kind of busy, and to be fair I think there is some overlap with the full page blog post

Create A BlogPost page and clean up BlogItem

Hmm… If you remember correctly in our last article, Routing works off a static <Switch> component. But we don’t want to have to hard code our every single blog post, that kind of defeats the purpose of using Contentful as CMS.

Luckily for us, react-router-dom has planned ahead and allows us to have path params. In Router.js add new route





<Switch><Route exact path='/' component={Home}/><Route exact path='/blog' component={Blog}/><Route path='/blog/:blogPost' component={BlogPost}/></Switch>

Important: This one took me a hot minute to figure our but you CAN NOT include a dash (-) in the parameter name. I originally had _:blog-post_ and couldn’t figure out why it looked like it was working but it wasn’t.

It’s also important to note that you need to add exact in the Blog path, otherwise it will match the _/blog_ route first.

Update the <Link …> in our BlogItem.js to put the /blog/ in front of our path








<Link className="level-item button is-small is-link is-outlined"to={{pathname: `/blog/${props.path}`,state: { props }}}>Read More</Link>

But before we can test this, we need to create a BlogPost page. For now lets just do some simple <pre> displaying so we can make sure we are sending the data to the blog post as state

import React from 'react'




const BlogPost = (props) => (<h1>Blog Post</h1><pre>{JSON.stringify(props, null, 2)}</pre>)

export default BlogPost

Now when we navigate to our blog post from the blog page we should see a huge object in our <pre> and the history object contains our state which is our blog post.

Set Up the Blog Page

Okay, before you read this and roar with rage that i clearly have no clue what I’m doing, this is not the best way to do this and object de-structuring this way can not be a best practice. This will all move to Redux in my next tutorial and which will be a WAY cleaner solution.





import React from 'react'import { Link } from 'react-router-dom'import moment from 'moment'import * as Markdown from 'react-markdown'import PageContent from './../components/PageContent'

const BlogPost = ({ location: { state: { props } }}) => {
































return (<PageContent><nav className="level"><div className="level-left"><Link className="level-item button is-small is-link is-outlined" to="/blog">Back to Blog</Link></div><div className="level-right"><p className="level-item has-text-link is-size-7"> {moment(props.date).calendar(null, {sameDay: '[Today]',lastDay: '[Yesterday]',lastWeek: '[Last] dddd',sameElse: 'MMM Do YYYY'})}</p></div></nav><article className="media"><div className="media-left"><figure className="image is-64x64"><img src={props.icon} alt="Image" /></figure></div><div className="media-content"><div className="content"><h1>{props.title}</h1><Markdown source={props.content} /></div></div></article></PageContent>)}

export default BlogPost

Clean Up the Overlap

Now we can look at our BlogItem.js and BlogPost.js and find out overlapping code and make them nice pretty little components.

Both our pages contain a content sections with the icon and and bottom and date sections, however, the button and date sections are in different places on the two pages.

Let’s first create a folder for our new components. These are share components of the Blog so in the blog folder create a shared folder, then create two files BlogContent.js and BlogNav.js



import React from 'react'import { Link } from 'react-router-dom'import moment from 'moment'

















const BlogNav = ({ to, date }) => (<nav className="level"><div className="level-left"><Link className="level-item button is-small is-link is-outlined" to={to}>Back to Blog</Link></div><div className="level-right"><p className="level-item has-text-link is-size-7">{moment(date).calendar(null, {sameDay: '[Today]',lastDay: '[Yesterday]',lastWeek: '[Last] dddd',sameElse: 'MMM Do YYYY'})}</p></div></nav>)

export default BlogNav

and


import React from 'react'import * as Markdown from 'react-markdown'






















const BlogContent = (props) => (<article className="media"><div className="media-left"><figure className="image is-64x64"><img src={props.icon} alt="Image" /></figure></div><div className="media-content"><div className="content"><h1>{props.title}</h1><Markdownsource={props.limit? props.content.split(" ").splice(0,props.limit).join(" ").concat('...'): props.content}/></div>{ props.children }</div></article>)

export default BlogContent

In BlogContent.js, we won’t always use **props.children** but we want to put it there so we can place the nav in the that location on BlogItems.js. You’ll see…

Now, lets update our BlogItem.js and BlogPost.js. This is gonna blow your mind.



import React from 'react'import BlogNav from './shared/BlogNav'import BlogContent from './shared/BlogContent'










const BlogItem = (props) => (<div className="box"><BlogContent limit={150} {...props }><BlogNav date={props.date} to={{pathname: `/blog/${props.path}`,state: { props }}} /></BlogContent></div>)

export default BlogItem

Notice how we are wrapping the BlogNav in this snippet, but not in the next one. This is what the props.children is used for.

and




import React from 'react'import PageContent from './../components/PageContent'import BlogNav from './shared/BlogNav'import BlogContent from './shared/BlogContent'






const BlogPost = ({ location: { state: { props } }}) => (<PageContent><BlogNav date={props.date} to="/blog" /><BlogContent {...props } /></PageContent>)

export default BlogPost

That’s SO much better!

The Status Tag

In my examples I am including a tag for Status, this way I can tag my posts as in progress or archived or whatever

You don’t know this yet but I do plan to use this in other places so we are going to add this in the src/components folder. Create a file called StatusTag.js.

import React from 'react'










const StatusTag = ({status}) => {switch(status) {case 'IN_PROGRESS':return (<span className="tag is-small is-warning" style={{ marginRight: 20 }}>In Progress</span>)case 'ARCHIVED':return (<span className="tag is-small is-danger" style={{ marginRight: 20 }}>Archived</span>)default:return (<span></span>)}}

export default StatusTag

And back in our BlogNav.js let’s add the StatusTag next to the date. And don’t forget to import it.

import StatusTag from './../../components/StatusTag'


















const BlogNav = ({ to, date, status }) => (<nav className="level"><div className="level-left"><Link className="level-item button is-small is-link is-outlined" to={to}>Back to Blog</Link></div><div className="level-right"><StatusTag status={status} /><p className="level-item has-text-link is-size-7">{moment(date).calendar(null, {sameDay: '[Today]',lastDay: '[Yesterday]',lastWeek: '[Last] dddd',sameElse: 'MMM Do YYYY'})}</p></div></nav>)

And we need to update places we use the <BlogNav … > component.

<BlogNav date={props.date} status={props.status} to="/blog" />

Now we have a nice little tag so we can let our readers know that we are still working on this article.

Let’s Review

  • We learned about Contentful and signed up for an account
  • Created Contentful content
  • Connect Contentful to our app
  • Created a blog feed page
  • Created a blog feed items
  • Created a blog post page
  • Updated our Contentful content to realistic content
  • Set up React to render Markdown as Html
  • Refacted our code to make it cleaner
  • Added a blog Status tag

Now, we have the basic blog that you can start using today if you wanted

Next — Import your Medium Feed into React