Create React App is a great tool that lets you start a new React application easily. There are some limitations though that you need to be aware of:
-
Your app is rendered entirely on the client. While usually search engines can run JavaScript, if your app is mostly static (for example, a blog) there are advantages in simply serving an HTML file rendered by a server.
-
You can have multiple HTML files but your JavaScript will only be injected into the
index.html
file. If you are creating a blog for example, you probably want a different<title>
and Open Graph metatags for each post and, while you can do that with JavaScript using react-helmet, my experience is that most platforms (like Twitter, Facebook and Slack) won't pick those, as they don't execute JavaScript. -
If you are planning to deploy to GitHub Pages or a similar static hosting provider, keep in mind that if your client-side router uses the HTML5 pushState history API, your users may run into 404s on fresh page loads.
http://user.github.io/posts/hello-world
will make the GitHub Pages server search for aposts/hello-world/index.html
file that doesn't exist. This is important for all sorts of apps, including blogs.
So now that we're aware of the limitations, let's build a blog and try to address them.
We begin with the regular boilerplate. Make sure you have recent versions of Node and NPM.
npx create-react-app my-blog
cd my-blog
npm start
The app should now be running. Let's remove some of the generated files, including the registerServiceWorker.js
file.
Service Workers are great, but it's easy to run into problems if you're unexperienced with them.
Remove and simplify everything until the src
folder looks like this:
/* index.js */
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./index.css";
ReactDOM.render(<App />, document.getElementById("root"));
/* index.css */
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
/* App.js */
import React from "react";
export default () => <div />;
Now we npm install react-router-dom
so that we can have multiple pages.
We will have one for the post listing and one per post.
/* App.js */
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Posts from "./Posts";
import Post from "./Post";
import NotFound from "./NotFound";
import data from "./data";
export default () => (
<Router>
<Switch>
<Route exact path="/" render={routeProps => <Posts {...data} />} />
{Object.entries(data.posts).map(([slug, post]) => (
<Route
key={slug}
exact
path={`/${slug}`}
render={({ match }) => <Post {...post} />}
/>
))}
<Route render={routeProps => <NotFound />} />
</Switch>
</Router>
);
/* data.js */
export default {
posts: {
"creating-blog-with-cra-and-github": {
date: "2018-02-18",
title: "Creating a blog with create-react-app and GitHub",
summary:
"Create React App is a great tool that lets you start a new React application very easily. There are some limitations though that you need to be aware of.",
},
"dear-hume": {
date: "1958-04-22",
title: "Dear Hume",
summary:
"You ask advice: ah, what a very human and very dangerous thing to do! For to give advice to a man who asks what to do with his life implies something very close to egomania. To presume to point a man to the right and ultimate goal -- to point with a trembling finger in the right direction is something only a fool would take upon himself.",
},
},
};
/* NotFound.js */
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
export default () => (
<Fragment>
The page you are looking for was moved, removed,
renamed or might never existed. <br />
<NavLink to="/">Back to blog</NavLink>
</Fragment>
);
/* Posts.js */
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
export default ({ posts }) => (
<Fragment>
<h1>Blog</h1>
<ol>
{Object.entries(posts).map(([slug, post]) => (
<li key={slug}>
<h2>
<NavLink to={slug}>{post.title}</NavLink>
</h2>
<p>{post.summary}</p>
<em>{post.date}</em>
</li>
))}
</ol>
</Fragment>
);
/* Post.js */
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
export default ({ date, title }) => (
<Fragment>
<h1>
<NavLink to="/">Blog</NavLink>
</h1>
<h2>{title}</h2>
<em>{date}</em>
</Fragment>
);
You should now be able to navigate through the example posts.
Let's also npm install react-helmet
to manage our head
tags.
We'll only use it for <title>
but you may want to add Open Graph and Twitter tags too.
# App.js
-import React from "react";
+import React, { Fragment } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
+import { Helmet } from "react-helmet";
import Posts from "./Posts";
import Post from "./Post";
import NotFound from "./NotFound";
import data from "./data";
export default () => (
+ <Fragment>
+ <Helmet titleTemplate="%s | My Blog" />
+
<Router>
<Switch>
<Route exact path="/" render={routeProps => <Posts {...data} />} />
...
<Route render={routeProps => <NotFound {...data} />} />
</Switch>
</Router>
+ </Fragment>
);
# NotFound.js
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
+import { Helmet } from "react-helmet";
export default ({ nav }) => (
<Fragment>
+ <Helmet>
+ <title>404</title>
+ </Helmet>
+
The page you are looking for was moved, removed,
renamed or might never existed. <br />
<NavLink to="/">Back to blog</NavLink>
</Fragment>
# Posts.js
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
+import { Helmet } from "react-helmet";
export default ({ posts }) => (
<Fragment>
+ <Helmet>
+ <title>Posts</title>
+ </Helmet>
+
<h1>Blog</h1>
<ol>
# Post.js
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
+import { Helmet } from "react-helmet";
export default ({ gist, date, title, summary }) => (
<Fragment>
+ <Helmet>
+ <title>{title}</title>
+ </Helmet>
+
<h1>
<NavLink to="/">Blog</NavLink>
</h1>
I'm used to writing with Markdown. I'll also want syntax highlighting and basic styles for headings, images, lists, we are used to in GitHub readmes. Thankfully GitHub already does all of that and they offer an API, so let's use GitHub for:
- Writing and storing our posts using GitHub Gists
- Retrieve posts and render them using the GitHub API
- Use the stylesheet GitHub uses for rendering markdown files
We npm install github-markdown-css
and update our components accordingly:
# data.js
export default {
posts: {
"creating-blog-with-cra-and-github": {
+ gist: "f4f5311ad2ec25147bc458d791fdaeb5",
date: "2018-02-18",
title: "Creating a blog with create-react-app and GitHub",
summary:
"Create React App is a great tool that lets you start a new React application very easily. There are some limitations though that you need to be aware of.",
},
"dear-hume": {
+ gist: "150ed6aa20f9b72ef3fcaf39ac2f89c6",
date: "1958-04-22",
title: "Dear Hume",
summary:
# index.css
+@import "~github-markdown-css";
+
body {
margin: 0;
padding: 0;
/* Post.js */
import React, { Component, Fragment } from "react";
import { NavLink } from "react-router-dom";
import { Helmet } from "react-helmet";
const headers = { Accept: "application/vnd.github.v3.json" };
export default class Post extends Component {
state = {
content: null,
};
fetchData() {
return this.fetchGistMarkdownUrl(this.props.gist)
.then(this.fetchGistMarkdownText)
.then(this.fetchRenderedMarkdown);
}
fetchGistMarkdownUrl(id) {
return fetch(`https://api.github.com/gists/${id}`, { headers })
.then(response => response.json())
.then(json => Object.values(json.files)[0].raw_url);
}
fetchGistMarkdownText(rawUrl) {
return fetch(rawUrl).then(response => response.text());
}
fetchRenderedMarkdown(text) {
return fetch("https://api.github.com/markdown", {
headers,
method: "POST",
body: JSON.stringify({ text }),
}).then(response => response.text());
}
componentDidMount() {
this.fetchData().then(content => this.setState({ content }));
}
render() {
const { date, title } = this.props;
const { content } = this.state;
return (
<Fragment>
<Helmet>
<title>{title}</title>
</Helmet>
<h1>
<NavLink to="/">Blog</NavLink>
</h1>
<h2>{title}</h2>
<em>{date}</em>
<div
className="markdown-body"
dangerouslySetInnerHTML={{ __html: content }}
/>
</Fragment>
);
}
}
We are ready to deploy. I'll npm install gh-pages
to deploy to GitHub Pages more easily.
{
"name": "my-blog",
+ "homepage": "https://myusername.github.io/my-blog",
"version": "0.1.0",
"private": true,
"dependencies": {
+ "gh-pages": "^1.1.0",
"github-markdown-css": "^2.10.0",
...
},
"scripts": {
+ "predeploy": "npm run build",
+ "deploy": "gh-pages -d build",
"start": "react-scripts start",
If you are not using a custom domain or this is a GitHub Page for projects (that lives under a scope, like /my-blog
)
you'll need to specify that in the router. We can use a custom env variable for that:
# .env.production (in the root folder, the same as your package.json)
REACT_APP_BASENAME="/my-blog"
# App.js
<Fragment>
<Helmet titleTemplate="%s | My Blog" />
- <Router>
+ <Router basename={process.env.REACT_APP_BASENAME}>
<Switch>
<Route exact path="/" render={routeProps => <Posts {...data} />} />
Execute npm run deploy
and your blog should now be live.
It's time to address the limitations stated in the beginning of this post. There is an interesting project called React Snap that uses Puppeteer, a headless Chrome by the Google Chrome team, to pre-render an app like this into static HTML files.
Let's npm install react-snap
and update our package.json
:
// package.json
"react-router-dom": "^4.2.2",
- "react-scripts": "1.1.1"
+ "react-scripts": "1.1.1",
+ "react-snap": "^1.11.4"
},
"scripts": {
"predeploy": "npm run build",
"deploy": "gh-pages -d build",
"start": "react-scripts start",
- "build": "react-scripts build",
+ "build": "react-scripts build && react-snap",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
+ "reactSnap": {
+ "waitFor": 1000,
+ "preconnectThirdParty": false
}
}
This is great because it also solves the issue of using the HTML5 pushState history API, since we now have separate HTML files. Unrelated, but in case you need dynamic routes for an application, make sure to check the docs, specifically the section that mentions the spa-github-pages project.
This also means we don't need JavaScript on the client anymore, so we can safely remove the scripts with:
// package.json
"reactSnap": {
"waitFor": 1000,
- "preconnectThirdParty": false
+ "preconnectThirdParty": false,
+ "removeScriptTags": true
}
}
Finally, just so you don't hit GitHub API rate limits, create a personal token without any scopes (as it will be included inside the minified code) and use it in your build:
# .env.local (in the root folder, should be listed in .gitignore)
REACT_APP_ACCESS_TOKEN="your-github-access-token"
# Post.js
import React, { Component, Fragment } from "react";
import { NavLink } from "react-router-dom";
import { Helmet } from "react-helmet";
+import base64 from "base-64"; // Install this with `npm install base-64`
-const headers = { Accept: "application/vnd.github.v3.json" };
+const accessToken = process.env.REACT_APP_ACCESS_TOKEN;
+const headers = {
+ Accept: "application/vnd.github.v3.json",
+ Authorization: `Basic ${base64.encode(accessToken + ":")}`,
+};
That's it! You now have a blog built with JavaScript that your users can read, even if they have JavaScript disabled.
If this setup is good enough for you, great! But be sure to read about Gatsby. It's probably a better solution for a blog and may help with other important features, like RSS.
Thank you for reading. Hope this was of any help 👋
You can find a repository with this source and a demo here.