How to Build a Serverless, SEO-Friendly React Blog

Serverless application architectures are gaining in popularity and it’s no mystery why. Developers are able to build and iterate on products faster when they have less infrastructure to maintain and don’t need to worry about server maintenance, outages, and scaling bottlenecks.
In this tutorial, we are going to show you how to build a serverless, CMS-powered blog using React, ButterCMS, and built-in prerendering through Netlify. The finished code for this tutorial is available on Github.
ButterCMS is a hosted API-based CMS and content API that lets you build CMS-powered apps using any programming language including Ruby, Rails, Node.js, .NET, Python, Phoenix, Django, Flask, React, Angular, Go, PHP, Laravel, Elixir, and Meteor. Butter lets you manage content using a hosted dashboard and integrate it into your front-end of choice with our API — you can think of Butter as similar to WordPress except that you build your website in your language of choice and then plug-in the dynamic content using an API.
Netlify is a static website hosting service that streamlines integration with prerendering services like Prerender.io, SEO.js, and Brombone.
Getting Started
We’ll use the Create React App starter kit.
Install Create React App:
1 2 |
npm install -g create-react-app |
Then create the boilerplate for our app:
1 2 3 4 |
create-react-app react-serverless-blog cd react-serverless-blog npm start |
Adding Routing
Our blog needs two pages: one for listing all posts and another for displaying individual posts. Create BlogHome.js
and BlogPost.js
components in the src
directory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React, { Component } from 'react'; class BlogHome extends Component { render() { return ( <div> Home </div> ); } } export default BlogHome; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React, { Component } from 'react'; class BlogPost extends Component { render() { return ( <div> Post </div> ); } } export default BlogPost; |
Create React App doesn’t offer routing out-of-the-box so we’ll add react-router:
1 2 |
npm install react-router@3.0.3 --save |
In the source folder, create a new file called routes.js
. We’ll create routes for the blog home page with and without page parameters, as well as the individual post page:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<span class="pl-k">import</span> <span class="pl-smi">React</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>react<span class="pl-pds">'</span></span>; <span class="pl-k">import</span> { <span class="pl-smi">Router</span>, <span class="pl-smi">IndexRoute</span>, <span class="pl-smi">Route</span> } <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>react-router<span class="pl-pds">'</span></span>; <span class="pl-k">import</span> <span class="pl-smi">App</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>./App<span class="pl-pds">'</span></span>; <span class="pl-k">import</span> <span class="pl-smi">BlogHome</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>./BlogHome<span class="pl-pds">'</span></span>; <span class="pl-k">import</span> <span class="pl-smi">BlogPost</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>./BlogPost<span class="pl-pds">'</span></span>; <span class="pl-k">const</span> <span class="pl-c1">Routes</span> <span class="pl-k">=</span> (<span class="pl-smi">props</span>) <span class="pl-k">=></span> ( <span class="pl-k"><</span>Router {<span class="pl-k">...</span>props}<span class="pl-k">></span> <span class="pl-k"><</span>Route path<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>/<span class="pl-pds">"</span></span> component<span class="pl-k">=</span>{App}<span class="pl-k">></span> <span class="pl-k"><</span>IndexRoute component<span class="pl-k">=</span>{BlogHome} <span class="pl-k">/</span><span class="pl-k">></span> <span class="pl-k"><</span>Route path<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>/p/:page<span class="pl-pds">"</span></span> component<span class="pl-k">=</span>{BlogHome} <span class="pl-k">/</span><span class="pl-k">></span> <span class="pl-k"><</span>Route path<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>/post/:slug<span class="pl-pds">"</span></span> component<span class="pl-k">=</span>{BlogPost} <span class="pl-k">/</span><span class="pl-k">></span> <span class="pl-k"><</span><span class="pl-k">/</span>Route<span class="pl-k">></span> <span class="pl-k"><</span><span class="pl-k">/</span>Router<span class="pl-k">></span> ); <span class="pl-k">export</span> <span class="pl-c1">default</span> <span class="pl-smi">Routes</span>; |
Next, we’ll update index.js
so it uses our routes when initializing the application:
1 2 3 4 5 6 7 8 9 10 |
<span class="pl-k">import</span> <span class="pl-smi">React</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>react<span class="pl-pds">'</span></span>; <span class="pl-k">import</span> <span class="pl-smi">ReactDOM</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>react-dom<span class="pl-pds">'</span></span>; <span class="pl-k">import</span> { <span class="pl-smi">browserHistory</span> } <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>react-router<span class="pl-pds">'</span></span>; <span class="pl-k">import</span> <span class="pl-smi">Routes</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>./routes<span class="pl-pds">'</span></span>; <span class="pl-smi">ReactDOM</span>.<span class="pl-en">render</span>( <span class="pl-k"><</span>Routes history<span class="pl-k">=</span>{browserHistory} <span class="pl-k">/</span><span class="pl-k">></span>, <span class="pl-c1">document</span>.<span class="pl-c1">getElementById</span>(<span class="pl-s"><span class="pl-pds">'</span>root<span class="pl-pds">'</span></span>) ); |
And finally, we’ll update App.js
so it nests child components specified in our routes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<span class="pl-k">import</span> <span class="pl-smi">React</span>, { <span class="pl-smi">Component</span> } <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>react<span class="pl-pds">'</span></span>; <span class="pl-k">class</span> <span class="pl-en">App</span> <span class="pl-k">extends</span> <span class="pl-e">Component</span> { <span class="pl-en">render</span>() { <span class="pl-k">return</span> ( <span class="pl-k"><</span>div className<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>App<span class="pl-pds">"</span></span><span class="pl-k">></span> <span class="pl-k"><</span>div className<span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>App-header<span class="pl-pds">"</span></span><span class="pl-k">></span> <span class="pl-k"><</span>h2<span class="pl-k">></span>My blog<span class="pl-k"><</span><span class="pl-k">/</span>h2<span class="pl-k">></span> <span class="pl-k"><</span><span class="pl-k">/</span>div<span class="pl-k">></span> <span class="pl-k"><</span>div<span class="pl-k">></span> {<span class="pl-c1">this</span>.<span class="pl-smi">props</span>.<span class="pl-smi">children</span>} <span class="pl-k"><</span><span class="pl-k">/</span>div<span class="pl-k">></span> <span class="pl-k"><</span><span class="pl-k">/</span>div<span class="pl-k">></span> ); } } <span class="pl-k">export</span> <span class="pl-c1">default</span> <span class="pl-smi">App</span>; |
Building the Blog
We’ll use ButterCMS to build our blog. ButterCMS provides content APIs for blog posts, categories, tags, and authors.
First, we’ll install the ButterCMS Node.js API client:
1 2 |
npm install buttercms --save |
We’ll then update ‘BlogHome’ to fetch posts from ButterCMS and render them. Use the API token in the example below or get your own by signing into ButterCMS with Github.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
<span class="pl-k">import</span> <span class="pl-smi">React</span>, { <span class="pl-smi">Component</span> } <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>react<span class="pl-pds">'</span></span>; <span class="pl-k">import</span> { <span class="pl-smi">Link</span> } <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>react-router<span class="pl-pds">'</span></span> <span class="pl-k">import</span> <span class="pl-smi">Butter</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>buttercms<span class="pl-pds">'</span></span> <span class="pl-k">const</span> <span class="pl-c1">butter</span> <span class="pl-k">=</span> <span class="pl-en">Butter</span>(<span class="pl-s"><span class="pl-pds">'</span>de55d3f93789d4c5c26fb07445b680e8bca843bd<span class="pl-pds">'</span></span>); <span class="pl-k">class</span> <span class="pl-en">BlogHome</span> <span class="pl-k">extends</span> <span class="pl-e">Component</span> { <span class="pl-en">constructor</span>(<span class="pl-smi">props</span>) { <span class="pl-c1">super</span>(props); <span class="pl-c1">this</span>.<span class="pl-smi">state</span> <span class="pl-k">=</span> { loaded<span class="pl-k">:</span> <span class="pl-c1">false</span> }; } <span class="pl-en">fetchPosts</span>(<span class="pl-smi">page</span>) { <span class="pl-smi">butter</span>.<span class="pl-smi">post</span>.<span class="pl-en">list</span>({page<span class="pl-k">:</span> page, page_size<span class="pl-k">:</span> <span class="pl-c1">10</span>}).<span class="pl-en">then</span>((<span class="pl-smi">resp</span>) <span class="pl-k">=></span> { <span class="pl-c1">this</span>.<span class="pl-en">setState</span>({ loaded<span class="pl-k">:</span> <span class="pl-c1">true</span>, resp<span class="pl-k">:</span> <span class="pl-smi">resp</span>.<span class="pl-c1">data</span> }) }); } <span class="pl-en">componentWillMount</span>() { <span class="pl-k">let</span> page <span class="pl-k">=</span> <span class="pl-c1">this</span>.<span class="pl-smi">props</span>.<span class="pl-smi">params</span>.<span class="pl-smi">page</span> <span class="pl-k">||</span> <span class="pl-c1">1</span>; <span class="pl-c1">this</span>.<span class="pl-en">fetchPosts</span>(page) } <span class="pl-en">componentWillReceiveProps</span>(<span class="pl-smi">nextProps</span>) { <span class="pl-c1">this</span>.<span class="pl-en">setState</span>({loaded<span class="pl-k">:</span> <span class="pl-c1">false</span>}); <span class="pl-k">let</span> page <span class="pl-k">=</span> <span class="pl-smi">nextProps</span>.<span class="pl-smi">params</span>.<span class="pl-smi">page</span> <span class="pl-k">||</span> <span class="pl-c1">1</span>; <span class="pl-c1">this</span>.<span class="pl-en">fetchPosts</span>(page) } <span class="pl-en">render</span>() { <span class="pl-k">if</span> (<span class="pl-c1">this</span>.<span class="pl-smi">state</span>.<span class="pl-smi">loaded</span>) { <span class="pl-k">const</span> { <span class="pl-c1">next_page</span>, <span class="pl-c1">previous_page</span> } <span class="pl-k">=</span> <span class="pl-c1">this</span>.<span class="pl-smi">state</span>.<span class="pl-smi">resp</span>.<span class="pl-smi">meta</span>; <span class="pl-k">return</span> ( <span class="pl-k"><</span>div<span class="pl-k">></span> {<span class="pl-c1">this</span>.<span class="pl-smi">state</span>.<span class="pl-smi">resp</span>.<span class="pl-c1">data</span>.<span class="pl-en">map</span>((<span class="pl-smi">post</span>) <span class="pl-k">=></span> { <span class="pl-k">return</span> ( <span class="pl-k"><</span>div key<span class="pl-k">=</span>{<span class="pl-smi">post</span>.<span class="pl-smi">slug</span>}<span class="pl-k">></span> <span class="pl-k"><</span>Link to<span class="pl-k">=</span>{<span class="pl-s"><span class="pl-pds">`</span>/post/<span class="pl-s1"><span class="pl-pse">${</span><span class="pl-smi">post</span>.<span class="pl-smi">slug</span><span class="pl-pse">}</span></span><span class="pl-pds">`</span></span>}<span class="pl-k">></span>{<span class="pl-smi">post</span>.<span class="pl-c1">title</span>}<span class="pl-k"><</span><span class="pl-k">/</span>Link<span class="pl-k">></span> <span class="pl-k"><</span><span class="pl-k">/</span>div<span class="pl-k">></span> ) })} <span class="pl-k"><</span>br <span class="pl-k">/</span><span class="pl-k">></span> <span class="pl-k"><</span>div<span class="pl-k">></span> {previous_page <span class="pl-k">&&</span> <span class="pl-k"><</span>Link to<span class="pl-k">=</span>{<span class="pl-s"><span class="pl-pds">`</span>/p/<span class="pl-s1"><span class="pl-pse">${</span>previous_page<span class="pl-pse">}</span></span><span class="pl-pds">`</span></span>}<span class="pl-k">></span>Prev<span class="pl-k"><</span><span class="pl-k">/</span>Link<span class="pl-k">></span>} {next_page <span class="pl-k">&&</span> <span class="pl-k"><</span>Link to<span class="pl-k">=</span>{<span class="pl-s"><span class="pl-pds">`</span>/p/<span class="pl-s1"><span class="pl-pse">${</span>next_page<span class="pl-pse">}</span></span><span class="pl-pds">`</span></span>}<span class="pl-k">></span>Next<span class="pl-k"><</span><span class="pl-k">/</span>Link<span class="pl-k">></span>} <span class="pl-k"><</span><span class="pl-k">/</span>div<span class="pl-k">></span> <span class="pl-k"><</span><span class="pl-k">/</span>div<span class="pl-k">></span> ); } <span class="pl-k">else</span> { <span class="pl-k">return</span> ( <span class="pl-k"><</span>div<span class="pl-k">></span> Loading<span class="pl-k">...</span> <span class="pl-k"><</span><span class="pl-k">/</span>div<span class="pl-k">></span> ) } } } <span class="pl-k">export</span> <span class="pl-c1">default</span> <span class="pl-smi">BlogHome</span>; |
Next, we’ll update BlogPost.js
to fetch and display posts based on the route:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<span class="pl-k">import</span> <span class="pl-smi">React</span>, { <span class="pl-smi">Component</span> } <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>react<span class="pl-pds">'</span></span>; <span class="pl-k">import</span> <span class="pl-smi">Butter</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>buttercms<span class="pl-pds">'</span></span> <span class="pl-k">const</span> <span class="pl-c1">butter</span> <span class="pl-k">=</span> <span class="pl-en">Butter</span>(<span class="pl-s"><span class="pl-pds">'</span>de55d3f93789d4c5c26fb07445b680e8bca843bd<span class="pl-pds">'</span></span>); <span class="pl-k">class</span> <span class="pl-en">BlogPost</span> <span class="pl-k">extends</span> <span class="pl-e">Component</span> { <span class="pl-en">constructor</span>(<span class="pl-smi">props</span>) { <span class="pl-c1">super</span>(props); <span class="pl-c1">this</span>.<span class="pl-smi">state</span> <span class="pl-k">=</span> { loaded<span class="pl-k">:</span> <span class="pl-c1">false</span> }; } <span class="pl-en">componentWillMount</span>() { <span class="pl-k">let</span> slug <span class="pl-k">=</span> <span class="pl-c1">this</span>.<span class="pl-smi">props</span>.<span class="pl-smi">params</span>.<span class="pl-smi">slug</span>; <span class="pl-smi">butter</span>.<span class="pl-smi">post</span>.<span class="pl-en">retrieve</span>(slug).<span class="pl-en">then</span>((<span class="pl-smi">resp</span>) <span class="pl-k">=></span> { <span class="pl-c1">this</span>.<span class="pl-en">setState</span>({ loaded<span class="pl-k">:</span> <span class="pl-c1">true</span>, post<span class="pl-k">:</span> <span class="pl-smi">resp</span>.<span class="pl-c1">data</span>.<span class="pl-c1">data</span> }) }); } <span class="pl-en">render</span>() { <span class="pl-k">if</span> (<span class="pl-c1">this</span>.<span class="pl-smi">state</span>.<span class="pl-smi">loaded</span>) { <span class="pl-k">const</span> <span class="pl-c1">post</span> <span class="pl-k">=</span> <span class="pl-c1">this</span>.<span class="pl-smi">state</span>.<span class="pl-smi">post</span>; <span class="pl-k">return</span> ( <span class="pl-k"><</span>div<span class="pl-k">></span> <span class="pl-k"><</span>h1<span class="pl-k">></span>{<span class="pl-smi">post</span>.<span |