Introduction
We'll learn how we can build a blog site with Next.js and Strapi as our Headless CMS. With these two technologies combined, you can already have a blog up and ready as quickly as you can. So if you have opened this article so I assume you understand or familiar with the basics of React / Next js. With that all said, let's get started.
File Structure
This is going to be a monorepo so we can easily navigate through our backend (Strapi) and the frontend (Next.js).
Let's just have this simple file structure
- backend
- frontend
README.md
Installation
Open your terminal and create a directory
$ mkdir nextjs-strapi-blog
Then navigate into that directory and install Strapi and Next.js. For now let's put the --quickstart
flag, this basically just selects the database which will be SQLite and other default configurations just to setup our Strapi backend quick.
And of course, we can use any other SQL databases with Strapi.
$ npx create-strapi-app backend --quickstart
It will then take awhile for the Strapi installation so wait up for about 5 minutes maximum or less. Once that is done it will launch a page and asks you to create an admin account.
Just create a simple account that is easy to remember, for example:
First Name: Admin
Last Name: Admin
Email: admin@admin.com
Password: Password123!
Once that is done, the Strapi Admin dashboard should be opened by now.
Then up next will be to create our Next.js app
$ npx create-next-app frontend
After installing Next.js let's add TypeScript for our Next.js
$ touch tsconfig.json
Then run the app and it should throw us an error in the CLI and will ask us to install the following
# If you’re using npm
$ npm install --save-dev typescript @types/react @types/node
# If you’re using Yarn
$ yarn add --dev typescript @types/react @types/node
Once that is done we can run our Next.js server again and it should be ready. Then all of our files will end with .tsx
so we can use TypeScript in writing code and it will be much easier for us to write code for the application.
Creating a Post Collection
For a single post in our blog application, we'll have the following fields like title
, and content
. So that's all we have for now we'd like to keep it simple since this is just a simple blog application.
For our TypeScript datamodel, we'll have something like
export interface Post {
id: number;
title: string;
content: string;
created_at: any;
updated_at: any;
published_at: any;
}
The other fields like id
, created_at
and published_at
are being generated by Strapi.
So let's proceed to creating a Collection Type in Strapi. Now on the side menu / sidebar, hover over the "Content-Types Builder" and click it and it should navigate us to this page.
Once you are already in that page, then click on "Create new collection type"
A modal should then open with a field labelled as "Display Name", then just put "Post",
We want it to be in a form of a singular word than plural because Strapi will then read this as plural word when generating API endpoints. So basically if we have a Collection named as "Post" then our RESTful API endpoints that are generated will have /posts
, and /posts/:id
.
Click "Continue" to proceed.
While we only have two fields for this Collection, we simply want "Text" for our title
field and "Rich Text" for the content
.
Once that is done, click on "Save"
And after that, we already have a REST API that was generated by Strapi itself! We will also have the following CRUD feature up and ready, so let's visit the page under Strapi dashboard.
Then we can create a few posts then we'll test our API.
Creating Posts
Click the "Create" button on the top right portion and you should then navigate into this page with the form.
Click "Save" when done, then wait a bit and finally click "Publish" so we can see this getting returned from the REST API when we are requesting the data.
Allow read access to Public
Before anything else, we will have to allow reads. To do that, navigate into "Settings" page and click on the "Roles" tab under "Users & Permissions Plugin" section. Then on the table click on the row "Public" then we can allow reads publicly.
Once that is done be sure to click "Save", and we can proceed to testing our API manually in the browser or you can do it using Insomnia. Whichever you prefer.
Testing
Just to make it quick and easy because it's just basically the same thing. Open this in a new tab http://localhost:1337/posts
and it should return an array of objects.
Frontend
We can setup our frontend and make it read the posts that is created from Strapi. But before that I'll want to use axios
for HTTP calls.
So to install on a new fresh terminal and make sure you are under frontend
directory
$ cd frontend
Then install the package
$ npm install axios
For the look, let's use Chakra UI. To install it,
$ npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4
Then make the following changes of your Next application if you haven't already.
Change _app.js
to _app.tsx
then add the AppProps
type on the first destructured parameter.
Then the index.js
page to index.tsx
.
The moving back under _app.tsx
file, wrap the <Component {...pageProps} />
around the component ChakraProvider
It should then look like this when done correctly.
import { ChakraProvider } from "@chakra-ui/react";
import { AppProps } from "next/dist/next-server/lib/router/router";
function MyApp({ Component, pageProps }: AppProps) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
}
export default MyApp;
Then the index.tsx
file, remove everything from there and replace the following code:
import { GetServerSideProps, GetStaticProps } from "next";
import axios from "axios";
import { Box, Heading } from "@chakra-ui/layout";
interface Post {
id: number;
title: string;
content: string;
created_at: any;
updated_at: any;
published_at: any;
}
interface PostJsonResponse {
data: Post[];
}
export const getStaticProps: GetStaticProps = async () => {
const response = await axios.get("http://localhost:1337/posts", {
headers: {
Accept: "application/json",
},
});
const data: Post[] = response.data;
return {
props: {
data,
},
};
};
const IndexView = ({ data }: PostJsonResponse) => {
return (
<>
<Box height="100vh" padding="10">
<Heading>My Blog</Heading>
<pre>{JSON.stringify(data, null, 2)}</pre>
</Box>
</>
);
};
export default IndexView;
To break it down for you. Under index.tsx
file, that is our main view and the route path is /
, basically this is the first page.
We created an interface of Post
from the one I mentioned above and a PostJsonResponse
as we'll provide that type to the first parameter of our React component which is the props.
We also used getStaticProps
for fetching data from our Strapi backend. While this is just a simple blog application and there's not many posts to create we'll use getStaticProps
as it will pre generate these data during build time as JSON files. Basically making reads blazing fast.
And on the template, we used the Box
component from Chakra UI just for the layout and providing us padding and a height of 100vh
.
Then just to see the JSON data we called it in the template <pre>{JSON.stringify(data, null, 2)}</pre>
and the pre
tag just to make it look "pretty" and easier to read the JSON format.
So that's about it. So this is how it looks as of the moment.
Creating a PostCard component
Just to make things look better, let's create a PostCard
component that will have an onClick
prop so whenever we click on the card it will redirect us to a Post detail view to read more of the contents from each of our posts that we created from Strapi.
To do that, create a directory under frontend
directory and name it as components
then create the file called PostCard.tsx
.
Then the code would be as follows
import { Button } from "@chakra-ui/button";
import { Box, Heading, Text } from "@chakra-ui/layout";
export type PostCardProps = {
title: string;
publishedAt: string;
onClick: VoidFunction;
};
const PostCard = ({ title, publishedAt, onClick }: PostCardProps) => {
return (
<>
<Box
padding="30px"
width="500px"
shadow="lg"
borderRadius="md"
marginBottom="30px"
onClick={onClick}
>
<Box display="flex" justifyContent="space-between">
<Text fontWeight="bold" fontSize="24px">
{title}
</Text>
<Button colorScheme="facebook">Read</Button>
</Box>
<Text size="10px">Published at {new Date(publishedAt).toLocaleDateString()}</Text>
</Box>
</>
);
};
export default PostCard;
Use the PostCard component
Then head on over back to our index.tsx
file and update that code that will be using the newly created dumb component. It is a dumb component since it doesn't handle any state, only receiving input props from a parent component.
import { GetServerSideProps, GetStaticProps } from "next";
import { Box, Center, Heading, VStack } from "@chakra-ui/layout";
import { useRouter } from "next/router";
import axios from "axios";
import PostCard from "../components/PostCard";
interface Post {
id: number;
title: string;
content: string;
created_at: any;
updated_at: any;
published_at: any;
}
interface PostJsonResponse {
data: Post[];
}
export const getStaticProps: GetStaticProps = async () => {
const response = await axios.get("http://localhost:1337/posts", {
headers: {
Accept: "application/json",
},
});
const data: Post[] = response.data;
return {
props: {
data,
},
};
};
const IndexView = ({ data }: PostJsonResponse) => {
const router = useRouter();
const toPostView = (id: number) => router.push(`/posts/${id}`);
const posts = data.map((post) => (
<PostCard
key={post.id}
title={post.title}
publishedAt={post.published_at}
onClick={() => toPostView(post.id)}
/>
));
return (
<>
<Box height="100vh" padding="10">
<Heading>My Blog</Heading>
<Center>
<VStack>{posts}</VStack>
</Center>
</Box>
</>
);
};
export default IndexView;
And our application will look like this by now.
You may notice I have imported the useRouter()
hook from next/router
and I have put an on click handler on to the button "Read" and that it should navigate into the post detail view. When you click on it now, it will return you a 404 error.
So let's create that view.
Post detail view
Create a new folder under pages
directory and name it as posts
then create a file and name it as [id].tsx
where the brackets will make this view to render with dynamic route parameters. This way we can handle different Post IDs.
Then have the following code,
import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { Post } from "../../models/Post";
import { Button } from "@chakra-ui/button";
import { Box, Divider, Heading, Text } from "@chakra-ui/layout";
import axios from "axios";
export type PostDetailViewProps = {
data: Post;
};
export const getStaticPaths: GetStaticPaths = async () => {
const response = await axios.get("http://localhost:1337/posts");
const posts: Post[] = await response.data;
const paths = posts.map((post) => {
return {
params: { id: String(post.id) },
};
});
return {
paths,
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const { data } = await axios.get(`http://localhost:1337/posts/${params.id}`);
return {
props: {
data,
},
};
};
const PostDetailView = ({ data }: PostDetailViewProps) => {
const router = useRouter();
return (
<>
<Box padding="10">
<Button onClick={() => router.back()}>Back</Button>
<Heading>{data.title}</Heading>
<Text>{data.published_at}</Text>
<Divider marginTop="10" marginBottom="10"></Divider>
<Text>{data.content}</Text>
</Box>
</>
);
};
export default PostDetailView;
To break it down for you. We used getStaticPaths
to fetch all of the posts and map it down to shape as a path
that next
knows about. Since getStaticPaths
and getStaticProps
will be executed during build time and generates static content therefore it should make sense by now having called all posts inside a post detail view on getStaticPaths
.
We then used getStaticProps
and we have our first argument as the context but destructured it to only retrieve the params
property which have access to the parameters of the current route. That's how we retrieve the id
from the [id].tsx
file name. While we have that we can make a call to a specific post.
Then on the template, we just added a "Back" button so we imported useRouter
from next/router
, next is we display the title
field, published_at
field and then the content
. But for now I just didn't install a react markdown. Typically you should use react-markdown or any similar library to display the markdown contents properly.
This is how it looks by the way.
Summary
We learned how to build a blog using Strapi and Next.js and also understand some of the concepts Next.js has regarding getStaticProps
and getStaticPaths
for static site generation. By now you should be able to build out a simple blog on your own or you might a blog but has other use cases but simple CRUD functionalities are mostly required then usnig Strapi would definitely be a good pick. Otherwise if the project requires some customization, then consult Strapi's official documentation to understand/learn how you will implement it using Strapi.
If you've ever reached here at the bottom part of this article, then thank you so much for taking the time to read. Cheers and have a good day!
Full source code can be found from the repository.
Top comments (8)
Hi, ty for ur guide but i have this error:
Server Error
TypeError: data.map is not a function
const data is not an array i think, how is possible?
I copy\paste ur code..
Solved by myself, the problem was the new strapi api are different, the have data.attributes
how did you fixed it??
forum.strapi.io/t/why-am-i-getting...
I don’t remember the problem but i solves with thai post
Thanks for this article, but i have 1 doubt.
I have a blog application which have certain elements which are dynamic like reads, shares, etc. so how will i use these data to show dynamically by using getStaticProps as it makes the blog at build time only.
Please help me out here.
I believe this should help you out
youtube.com/watch?v=Sklc_fQBmcs
You'd probably need
getServerSideProps
Hi when I try to test the endpoints in a browser, I am getting 404 notfound error, any ideas?
api/posts