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.
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 ;)
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.
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.
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.
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!
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.
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
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!!!
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.
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.
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.
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 namedStore.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.
Back in Contentful, let’s add some fields to our Blog Content Model
We are going to add three new fields:
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!
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.
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
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.
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
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!
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.
Now, we have the basic blog that you can start using today if you wanted