Our community is looking for talented writers who are passionate about our ecosystem (Jamstack, open-source, javascript) and willing to share their knowledge/experiences through our Write for the community program.
Note: The content of this tutorial was revised and updated on February 2, 2022. Some other information such as the title might have been updated later.
If you are familiar with our blog you must have seen that we've released a series of tutorials on how to make blogs using Strapi with a lot of frontend frameworks:
React is an open-source, front-end, JavaScript library for building user interfaces or UI components. Facebook and a community of individual developers and companies maintain it. React can be used as a base in the development of single-page or mobile applications.
You may want to try the result of this tutorial directly. Well, we made a starter out of it so give it a try!
The goal here is to be able to create a simple static blog website using Strapi as the backend and React as the frontend We will make this tutorial shorter and more efficient using our new templates.
This tutorial will always use the latest version of Strapi. That is awesome, right!? You'll understand why below. You need to have node v.12 installed and that's all.
take blog-strapi
This is the easiest part of this tutorial, thanks to our expansion team who developed a series of Strapi templates that you can use for some different use cases.
These templates are Strapi applications containing existing collection types and single types suited for the appropriate use case and data. In this tutorial, we'll use the Blog template and connect a React application.
# Using yarn
yarn create strapi-app backend --quickstart --template @strapi/template-blog@1.0.0 blog
# Using npm
npx create-strapi-app backend --quickstart --template @strapi/template-blog@1.0.0 blog
Don't forget that Strapi is running on http://localhost:1337. Create your admin user by signing up!
That's it! You're done with Strapi! I'm not kidding; we can now create our React application to fetch our content from Strapi.
Ok ok, wait, let's talk about this fantastic template you just created.
You should know that before we had starters and templates we only had tutorials. The idea of creating starters came to us when we realized that we could do something with the end result of our tutorials. Thus our starters were born. However, Strapi evolves quickly, very quickly. At the time, starters were made up of a repository that included the backend as well as the frontend. This meant that updating the Strapi version on all our starters took too much time. Therefore we decided to develop templates that are always created with the latest versions of Strapi. This was achieved simply by passing the repository parameter to the desired template like you just did. This method also gives a recommended architecture for your Strapi project.
These templates provide a solid basis for your Strapi application and include the following:
Feel free to modify any of this if you want. However, for this tutorial, this initial setup should be enough.
Nice! Now that Strapi is ready, you will create your React application.
The easiest part has been completed; let's get our hands dirty developing our blog!
React setup
frontend
server by running the following command:# Using yarn
yarn create react-app frontend
# Using npm
npx create-react-app frontend
Once the installation is completed, you can start your front-end app to ensure everything went ok.
1cd frontend
2yarn dev
You are going to give a better structure to your React application! But first, let's install react-router-dom
react-router-dom
by running the following command:yarn add react-router-dom
Remove everything inside your src
folder
Create an index.js
file containing the following code:
1import React from "react";
2import ReactDOM from "react-dom";
3import App from "./containers/App";
4import { BrowserRouter as Router } from "react-router-dom";
5
6ReactDOM.render(
7 <Router>
8 <App />
9 </Router>,
10 document.getElementById("root")
11);
What you are doing here is wrapping your App
inside Router
. But what/where is your App
?
Well, we're going to create a containers
folder that will contain your App
containers/App
folder and a index.js
file inside containing the following code:1import React from "react";
2
3function App() {
4 return <div className="App" />;
5}
6
7export default App;
You should have an empty working React app from now. To make your blog look pretty, we will use a popular CSS framework for styling: UiKit and Apollo to query Strapi with GraphQL.
UIkit setup
public/index.html
to use UIkit from their CDN1...
2<title>React App</title>
3<link
4 rel="stylesheet"
5 href="https://fonts.googleapis.com/css?family=Staatliches"
6 />
7 <link
8 rel="stylesheet"
9 href="https://cdn.jsdelivr.net/npm/uikit@3.2.3/dist/css/uikit.min.css"
10 />
11
12 <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.2.0/js/uikit.min.js"></script>
13 <script src="https://cdn.jsdelivr.net/npm/uikit@3.2.3/dist/js/uikit-icons.min.js"></script>
14 <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.2.0/js/uikit.js"></script>
15...
./src/index.css
file containing the following style:1a {
2 text-decoration: none;
3}
4
5h1 {
6 font-family: Staatliches;
7 font-size: 120px;
8}
9
10#category {
11 font-family: Staatliches;
12 font-weight: 500;
13}
14
15#title {
16 letter-spacing: 0.4px;
17 font-size: 22px;
18 font-size: 1.375rem;
19 line-height: 1.13636;
20}
21
22#banner {
23 margin: 20px;
24 height: 800px;
25}
26
27#editor {
28 font-size: 16px;
29 font-size: 1rem;
30 line-height: 1.75;
31}
32
33.uk-navbar-container {
34 background: #fff !important;
35 font-family: Staatliches;
36}
37
38img:hover {
39 opacity: 1;
40 transition: opacity 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
41}
Please don't force me to explain you some CSS!
Apollo setup
yarn add apollo-boost @apollo/react-hooks graphql react-apollo
./src/utils/apolloClient.js
file containing the following code:1import { ApolloClient } from "apollo-client";
2import { InMemoryCache } from "apollo-cache-inmemory";
3import { HttpLink } from "apollo-link-http";
4
5const cache = new InMemoryCache();
6const link = new HttpLink({
7 uri: `${process.env.REACT_APP_BACKEND_URL}/graphql`
8});
9const client = new ApolloClient({
10 cache,
11 link
12});
13
14export default client;
Oh oh! It looks like you don't have any REACT_APP_BACKEND_URL
environment variable yet!
.env
file in the root of your application containing the following line:REACT_APP_BACKEND_URL=http://localhost:1337
You'll need to restart your server.
Note: You want Apollo to point to this address http://localhost:1337/graphql
. That's the one where you'll be able to fetch your data from your Strapi server.
App
inside the ApolloProvider
and import your index.css
file in the ./src/index.js
:1import React from "react";
2import ReactDOM from "react-dom";
3import { BrowserRouter as Router } from "react-router-dom";
4
5import { ApolloProvider } from "@apollo/react-hooks";
6
7import App from "./containers/App";
8
9import client from "./utils/apolloClient";
10
11import "./index.css";
12
13ReactDOM.render(
14 <Router>
15 <ApolloProvider client={client}>
16 <App />
17 </ApolloProvider>
18 </Router>,
19 document.getElementById("root")
20);
Awesome! Your app should be ready now! You should have a blank page if everything went ok
You are going to use Apollo to fetch your data from different pages. We don't want you to rewrite the same code every time on your pages. This is why you will write a Query
component that will be reusable!
./src/components/Query/index.js
file containing the following code:1import React from "react";
2import { useQuery } from "@apollo/react-hooks";
3
4const Query = ({ children, query, slug }) => {
5 const { data, loading, error } = useQuery(query, {
6 variables: { slug: slug }
7 });
8
9 if (loading) return <p>Loading...</p>;
10 if (error) return <p>Error: {JSON.stringify(error)}</p>;
11 return children({ data });
12};
13
14export default Query;
We use the useQuery
hook to call your Strapi server at this address http://localhost:1337/graphql
. We are sending an id
if it exists (it will be necessary when you want to fetch just one article).
If the request is successful, you will return the child component with the retrieved data as prop.
Let's try it out by creating your navbar that will fetch all our categories but first, let's create the GraphQL query
./src/queries/category/categories.js
file containing the following code:1import gql from "graphql-tag";
2
3const CATEGORIES_QUERY = gql`
4 query Categories {
5 categories {
6 data {
7 attributes {
8 slug
9 name
10 }
11 }
12 }
13 }
14`;
15
16export default CATEGORIES_QUERY;
Let's use this query to display your categories inside your navbar
./src/components/Nav/index.js
file containing the following code:1import React from "react";
2import Query from "../Query";
3import { Link } from "react-router-dom";
4
5import CATEGORIES_QUERY from "../../queries/category/categories";
6
7const Nav = () => {
8 return (
9 <div>
10 <Query query={CATEGORIES_QUERY} id={null}>
11 {({ data: { categories } }) => {
12 return (
13 <div>
14 <nav className="uk-navbar-container" data-uk-navbar>
15 <div className="uk-navbar-left">
16 <ul className="uk-navbar-nav">
17 <li>
18 <Link to="/">Strapi Blog</Link>
19 </li>
20 </ul>
21 </div>
22
23 <div className="uk-navbar-right">
24 <ul className="uk-navbar-nav">
25 {categories.data.map((category) => {
26 return (
27 <li key={category.attributes.slug}>
28 <Link
29 to={`/category/${category.attributes.slug}`}
30 className="uk-link-reset"
31 >
32 {category.attributes.name}
33 </Link>
34 </li>
35 );
36 })}
37 </ul>
38 </div>
39 </nav>
40 </div>
41 );
42 }}
43 </Query>
44 </div>
45 );
46};
47
48export default Nav;
Since we want our Nav to be on every pages of our application we are going to use it inside our App container
containers/App/index.js
1import React from "react";
2import Nav from "../../components/Nav";
3
4function App() {
5 return (
6 <div className="App">
7 <Nav />
8 </div>
9 );
10}
11
12export default App;
Great! You should now be able to see your brand new navbar containing your categories. But the links are not working right now. We'll work on this later in the tutorial, don't worry.
Note: The current code is unsuitable for displaying many categories as you may encounter a UI issue. Since this blog post is supposed to be short, you could improve the code by adding a lazy load.
This container will wrap a component that will display all your articles.
containers/Articles/index.js
file containing the following code:1import React from "react";
2import Articles from "../../components/Articles";
3import Query from "../../components/Query";
4import ARTICLES_QUERY from "../../queries/article/articles";
5
6const Home = () => {
7 return (
8 <div>
9 <div className="uk-section">
10 <div className="uk-container uk-container-large">
11 <h1>Strapi blog</h1>
12 <Query query={ARTICLES_QUERY}>
13 {({ data: { articles } }) => {
14 return <Articles articles={articles.data} />;
15 }}
16 </Query>
17 </div>
18 </div>
19 </div>
20 );
21};
22
23export default Home;
Let's write the query that fetches all your articles.
./src/queries/article/articles.js
file containing the following code:1import gql from "graphql-tag";
2
3const ARTICLES_QUERY = gql`
4 query Articles {
5 articles {
6 data {
7 attributes {
8 slug
9 title
10 category {
11 data {
12 attributes {
13 slug
14 name
15 }
16 }
17 }
18 image {
19 data {
20 attributes {
21 url
22 }
23 }
24 }
25 }
26 }
27 }
28 }
29`;
30
31export default ARTICLES_QUERY;
Now we need to create an Articles
component that will display all of your articles and a Card component that will display each of your articles:
components/Articles/index.js
file containing the following:1import React from "react";
2import Card from "../Card";
3
4const Articles = ({ articles }) => {
5 const leftArticlesCount = Math.ceil(articles.length / 5);
6 const leftArticles = articles.slice(0, leftArticlesCount);
7 const rightArticles = articles.slice(
8 leftArticlesCount,
9 articles.length
10 );
11
12 return (
13 <div>
14 <div className="uk-child-width-1-2" data-uk-grid>
15 <div>
16 {leftArticles.map((article) => {
17 return (
18 <Card
19 article={article}
20 key={`article__${article.attributes.slug}`}
21 />
22 );
23 })}
24 </div>
25 <div>
26 <div className="uk-child-width-1-2@m uk-grid-match" data-uk-grid>
27 {rightArticles.map((article) => {
28 return (
29 <Card
30 article={article}
31 key={`article__${article.attributes.slug}`}
32 />
33 );
34 })}
35 </div>
36 </div>
37 </div>
38 </div>
39 );
40};
41
42export default Articles;
components/Card/index.js
file containing the following code:1import React from "react";
2import { Link } from "react-router-dom";
3
4const Card = ({ article }) => {
5 const imageUrl =
6 process.env.NODE_ENV !== "development"
7 ? article.attributes.image.data.attributes.url
8 : process.env.REACT_APP_BACKEND_URL +
9 article.attributes.image.data.attributes.url;
10 return (
11 <Link to={`/article/${article.attributes.slug}`} className="uk-link-reset">
12 <div className="uk-card uk-card-muted">
13 <div className="uk-card-media-top">
14 <img src={imageUrl} alt={article.attributes.image.url} height="100" />
15 </div>
16 <div className="uk-card-body">
17 <p id="category" className="uk-text-uppercase">
18 {article.attributes.category.data.attributes.name}
19 </p>
20 <p id="title" className="uk-text-large">
21 {article.attributes.title}
22 </p>
23 </div>
24 </div>
25 </Link>
26 );
27};
28
29export default Card;
Everything is ready, we just need to import the Articles container inside the App container.
Articles
container inside your containers/App/index.js
:1import React from "react";
2
3import Articles from "../Articles";
4import Nav from "../../components/Nav";
5
6function App() {
7 return (
8 <div className="App">
9 <Nav />
10 <Articles />
11 </div>
12 );
13}
14
15export default App;
You can see that if you click on the article, there is nothing. Let's create the article container together! But first, you'll need two packages:
react-moment
and react-markdown
by running the following command:yarn add react-moment react-markdown moment
react-moment
will allow you to display the publication date of your article, and react-markdown
will be used to display the content of your article in markdown.
./containers/Article/index.js
file containing the following:1import React from "react";
2import { useParams } from "react-router";
3import Query from "../../components/Query";
4import ReactMarkdown from "react-markdown";
5import Moment from "react-moment";
6
7import ARTICLE_QUERY from "../../queries/article/article";
8
9const Article = () => {
10 let { slug } = useParams();
11
12 return (
13 <Query query={ARTICLE_QUERY} slug={slug}>
14 {({ data: { articles } }) => {
15 if (articles.data.length) {
16 const imageUrl =
17 process.env.NODE_ENV !== "development"
18 ? articles.data[0].attributes.image.data.attributes.url
19 : process.env.REACT_APP_BACKEND_URL +
20 articles.data[0].attributes.image.data.attributes.url;
21
22 return (
23 <div>
24 <div
25 id="banner"
26 className="uk-height-medium uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light uk-padding uk-margin"
27 data-src={imageUrl}
28 data-srcset={imageUrl}
29 data-uk-img
30 >
31 <h1>{articles.data[0].attributes.title}</h1>
32 </div>
33
34 <div className="uk-section">
35 <div className="uk-container uk-container-small">
36 <ReactMarkdown source={articles.data[0].attributes.content} />
37 <p>
38 <Moment format="MMM Do YYYY">
39 {articles.data[0].attributes.published_at}
40 </Moment>
41 </p>
42 </div>
43 </div>
44 </div>
45 );
46 }
47 }}
48 </Query>
49 );
50};
51
52export default Article;
Let's write the query for just one article now!
./src/queries/article/article.js
containing the following code:1import gql from "graphql-tag";
2
3const ARTICLE_QUERY = gql`
4 query Article($slug: String!) {
5 articles(filters: { slug: { eq: $slug } }) {
6 data {
7 attributes {
8 slug
9 title
10 category {
11 data {
12 attributes {
13 slug
14 name
15 }
16 }
17 }
18 image {
19 data {
20 attributes {
21 url
22 }
23 }
24 }
25 }
26 }
27 }
28 }
29`;
30
31export default ARTICLE_QUERY;
The article page is ready, we just need to add this new Article container inside the App container. You are going to use the Switch and Route components from react-router-dom
in order to establish a routing system for your article page
Article
container inside your containers/App/index.js
:1import React from "react";
2
3import { Routes, Route } from "react-router-dom";
4
5import Nav from "../../components/Nav";
6import Articles from "../Articles";
7import Article from "../Article";
8
9function App() {
10 return (
11 <div className="App">
12 <Nav />
13 <Routes>
14 <Route path="/" element={<Articles />} exact />
15 <Route path="/article/:slug" element={<Article />} exact />
16 </Routes>
17 </div>
18 );
19}
20
21export default App;
Great! You should be able to get your article now!
You may want to separate your article depending on the categories! Let's create a page for each category then:
./containers/Category/index.js
file containing the following:1import React from "react";
2import { useParams } from "react-router";
3import Articles from "../../components/Articles";
4import Query from "../../components/Query";
5import CATEGORY_ARTICLES_QUERY from "../../queries/category/articles";
6
7const Category = () => {
8 let { slug } = useParams();
9
10 return (
11 <Query query={CATEGORY_ARTICLES_QUERY} slug={slug}>
12 {({ data: { categories } }) => {
13 if (categories.data.length) {
14 return (
15 <div>
16 <div className="uk-section">
17 <div className="uk-container uk-container-large">
18 <h1>{categories.data[0].attributes.name}</h1>
19 <Articles articles={categories.data[0].attributes.articles.data} />
20 </div>
21 </div>
22 </div>
23 );
24 }
25 }}
26 </Query>
27 );
28};
29
30export default Category;
./src/queries/category/articles.js
file containing the following:1import gql from "graphql-tag";
2
3const CATEGORY_ARTICLES_QUERY = gql`
4 query Category($slug: String!) {
5 categories(filters: { slug: { eq: $slug } }) {
6 data {
7 attributes {
8 slug
9 name
10 articles {
11 data {
12 attributes {
13 slug
14 title
15 content
16 category {
17 data {
18 attributes {
19 name
20 }
21 }
22 }
23 image {
24 data {
25 attributes {
26 url
27 }
28 }
29 }
30 }
31 }
32 }
33 }
34 }
35 }
36 }
37`;
38
39export default CATEGORY_ARTICLES_QUERY;
Category page is ready, we just need to add this new container inside the App container.
Category
container inside your containers/App/index.js
:1import React from "react";
2
3import { Routes, Route } from "react-router-dom";
4
5import Nav from "../../components/Nav";
6import Articles from "../Articles";
7import Article from "../Article";
8import Category from "../Category";
9
10function App() {
11 return (
12 <div className="App">
13 <Nav />
14 <Routes>
15 <Route path="/" element={<Articles />} exact />
16 <Route path="/article/:slug" element={<Article />} exact />
17 <Route path="/category/:slug" element={<Category />} exact />
18 </Routes>
19 </div>
20 );
21}
22
23export default App;
Awesome! You can now list articles depending on the selected category.
Huge congrats, you successfully achieved this tutorial. I hope you enjoyed it!
Still hungry?
Feel free to add additional features, adapt this project to your needs, and give feedback in the comment section below. For example, the template used in this tutorial has an SEO component that is not used in the front end. Feel free to include it, but we advise using Gatsby or Next.js if you want to have an SEO-friendly blog.
Please note: Since we initially published this blog, we released new versions of Strapi, and tutorials may be outdated. Sorry for the inconvenience if it's the case, and please help us by reporting it here.
This article was updated on February 15th, 2021
Get started with Strapi by creating a project using a starter or trying our live demo. Also, consult our forum if you have any questions. We will be there to help you.
See you soon!Maxime started to code in 2015 and quickly joined the Growth team of Strapi. He particularly likes to create useful content for the awesome Strapi community. Send him a meme on Twitter to make his day: @MaxCastres