First things first, let's introduce the stack we are going to use. Typescript is "Is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale." in simple words, Typescript enhances coding safety and enables more OOP for the web.
NextJS is a framework that "Enables you to create full-stack web applications by extending the latest React features" in simple words, NextJS is an upgrade over the widely used ReactJS library.
Strapi is an open-source headless CMS. CMS stands for "content management system," allowing us to create, manage, modify, and publish content in a user-friendly interface without programming knowledge. Headless means there is no built-in end-user UI; we can use whatever frontend technology we want.
GraphQL is "A query language for APIs and a runtime for fulfilling those queries with your existing data" in simple words, it's a language used to query data and get the fields you want.
Note: this is an overview, which means there are aspects I will not go over, such as styling and best HTML tags; this blog is meant to showcase the functionality of creating a Blog using the technologies mentioned above.
Initializing The Project
Now, let's create the folders using the command line (because it's cooler), so the first command is to create a folder containing all our files.
mkdir my-personal-website
Now, let's create folders for both NextJS and Strapi.
cd my-personal-website
mkdir frontend, strapi
Initialize Strapi
cd strapi
npx create-strapi-app@latest . --ts --quickstart
Parameter @latest
means it will install the latest version, .
means we want to create the project in the current folder, then — ts
means it will use TypeScript, and lastly — quickstart
it will install with default settings; when the installation finishes, it will open a new window on the address /admin/auth/register-admin
, asking you to create a new admin account; go ahead and create a new account. Note: using — ts
is optional since I won't change the source code in this post.
Initialize NextJS
cd frontend
npx create-next-app@latest . --ts
And lastly, remember to init a Github repo, but I'll not cover it in this post.
Starting with Strapi
I like to start with the backend in my projects, so I will start with Strapi as it's considered a backend.
Installing Graphql
One advantage of using Strapi is its easy integration with Graphql. To install the Graphql plugin, we need the run the following command.
npm run strapi install graphql
If you encounter the _Duplicate "graphql" modules
error, don't worry; it's easy to fix; all you need to do is to delete the node_modules folder and package-lock.json , then we need to reinstall packages.
rm node_modules
rm package-lock.json
npm install
npm run strapi dev
After successful installation, we can now visit the URL /graphql
.
Creating The Blog Post Collection
Open the Strapi homepage on URL http://localhost:1337/admin/
, and after you sign in, go to the Content-Type Builder page, click on Create new collection type button, then in the Display name field, fill it in with an appropriate name; I will choose Blog Post , then click continue.
Now, we are in fields selection for our new collection; you can customize it however you want; for the sake of simplicity, I will add three fields, a Text field for the title, another Text field for the description, and lastly, a Rich Text field for the body, next click save.
Now that we have a collection, we need to populate it with data, go to Content Manager , and now we can see the newly created collection, click on it, then Create new entry button, and fill it with any data you want, then click save and publish.
Accessibility Option 1: Publicly Accessible
Before we can access our data through Graphql, we need to make it accessible; you can make it publicly accessible or only for authenticated users. I will choose a publicly accessible collection since I'm not storing sensitive data.
To access it publicly, go to Settings, then roles, click on Public , scroll down to the collection you created (in my case, it's Blog-post), click it, then tick find and findOne.
Accessibility Option 2: Authenticated Only
However, should you choose the authenticated way, here is how to do it. There are two ways to achieve this; the first one is to create API Token in the settings page, then we need to attach it with our requests by adding it to the headers as an Authorization header and setting the value to be bearer , the second way is to create a new entry in the User collection, via Content Manager page, then send a Post request to /api/auth/local
with an attached JSON body containing identifier which could be either the username or email, and password, of the newly created user in the User collection, NOT THE ADMIN ACCOUNT WE CREATED IN INITIALIZE STRAPI.
{
"identifier": "email or username",
"password": "password"
}
If the request were successful, you would get a response containing the JWT field, and now we need to do the same thing, send it with each request via headers in an Authorization header and set the value to be bearer .
Now to access it, go to Settings, then roles, click on Authenticated , scroll down to the collection you created (in my case, it's Blog-post), click it, then tick find and findOne.
Testing Graphql Endpoint
To access Graphql go to /graphql
, and now, we can see the input field on the left side; here, we will put our queries to query our Strapi application, an example of a query:
query {
blogPosts {
data {
id
attributes {
title
description
body
}
}
}
}
And in my case, the response is:
"data": {
"blogPosts": {
"data": [
{
"id": "1",
"attributes": {
"title": "test",
"description": "test",
"body": "test"
}
}
]
}
}
}
Testing Rest Endpoint
All we need to do is send a GET request to /api/blog-posts/
. An example of a response would be:
{
"data": [
{
"id": 1,
"attributes": {
"title": "test",
"description": "test",
"body": "test",
"createdAt": "2023-01-30T20:28:28.141Z",
"updatedAt": "2023-01-30T20:28:29.027Z",
"publishedAt": "2023-01-30T20:28:29.025Z"
}
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 1
}
}
}
We have finished the Strapi section and will continue with NextJS.
Starting With NextJS
Create a new directory inside the pages directory and name it blog ; next, create a new index.tsx
file and initialize a basic react component.
import {NextPage} from "next";
interface Props {
}
const About: NextPage<Props> = (Props) => {
return (
<p>About Page</p>
)
}
export default About;
Now we are to visit blog
and view the About Page.
Creating Interfaces
We will need a few interfaces; let's start by creating a common folder containing all of our basic modules, then create another folder called types which will contain all our types and interfaces then create a new typescript I will name BlogInterfaces ; now let the first interface, which is going to IBlogIdentification
and it will contain our identification field; in our case, it's the id of the blog post.
export interface IBlogIdentification {
id: string
}
Next, we need an interface to hold the blog fields' title, description, and body; name it IBlogFields
.
export interface IBlogFields {
title: string,
description: string,
body: string
}
Next, we need an interface for blog attributes we will receive from the GraphQL query; name it IBlogAttributes
.
export interface IBlogAttributes {
attributes: Partial<IBlogFields> & Pick<IBlogFields, 'title'>
}
I have used Partial and Pick utility types to make all of IBlogFields
fields optional except title.
Lastly, we will need an interface to handle the whole blog entry; name it IBlog
.
export interface IBlog extends IBlogIdentification, IBlogAttributes{}
Now let's update the Props interface on the blog page.
interface Props {
blogs: IBlog[]
}
GraphQL Works
Before we start fetching data, we need to do GraphQL Works. First, we need to install two packages to be able to interact with GraphQL; we will need the graphql
and @apollo/client packages
.
npm i graphql, @apollo/client
Second, we need to write the queries; we will need three queries, the first to list blogs, the second is to get a single blog, and the third is to get the list of blog Ids'; in the common folder, create a new folder and call it graphql, then create a new file and call it queries.tsx
, and add the following queries.
export const LIST_BLOG = gql(`query {
blogPosts {
data {
id
attributes {
title
}
}
}
}`)
export const SINGLE_BLOG = gql(`query ($blogId: string!) {
blogPosts(filters: {id: {eq: $blogId}}) {
data {
id
attributes {
title
description
body
}
}
}
}`)
export const LIST_ID = gql(`query {
blogPosts {
data {
id
}
}
}`)
The third thing we need is to store Strapi's address, and the most convenient way is to store it inside the env variables; NextJS already comes with a built-in environment variable module, so there is no need to install dotenv
, all we need to do is to go to next-config.js
, and add the following export.
module.exports = {
env: {
STRAPI_ADDRESS: "http://127.0.0.1:1337/graphql"
},
nextConfig,
}
And the last thing we need is an ApolloClient
to be to connect with the GraphQL server, inside the graphql folder, create a new file and name it client.tsx
, then add the following code.
import {ApolloClient, InMemoryCache, NormalizedCacheObject} from "@apollo/client";
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
uri: process.env.STRAPI_ADDRESS,
cache: new InMemoryCache(),
});
export default client;
Note: the cache is a way for the ApolloClient
to cache results, it's a big topic and out of scope for this post. More info can be found here.
Fetching Data
Now we can start implementing the fetching procedure; let's start by creating the getStaticProps
function:
export const getStaticProps: GetStaticProps<Props> = async () => {
}
We are going to use getStaticProps
because it will pre-render the page at build time, and the data will not be changing consistently; there is no need to send a request to our CMS every time.
Now we can start querying Strapi; let's get a list of blogs.
const {data} = await client.query({query: LIST_BLOG})
const blogs: IBlog[] = data.blogPosts.data;
return {
props: {
blogs
}
}
Displaying Blog List
Now that we can retrieve a list of blogs, we can start displaying them; we need new folders. To make it easier, I will show a demo below of my project structure.
common
--elements
----blog
------BlogTitle.tsx
modules
--blog
----BlogCardList.tsx
We need two files, BlogTitle
will render the title of a blog, and BlogCardList
will use BlogTitle
to render a list of blogs, let's start with BlogTitle
.
interface Props {
title: string
isClickable: boolean
blogId?: string
}
const BlogTitle: NextPage<Props> = ({title, isClickable, blogId}) => {
if(isClickable && blogId) {
return (
<Link href={`/blog/${blogId}`}>{title}</Link>
)
} else {
return (
<p>{title}</p>
)
}
}
Note: Because I know in the commercial version, I will be using the blog title in multiple locations, and it will have more props and functionality attached to it, I chose to make the blog title a separate component; however, you can choose not to.
Now to the list of cards.
interface Props {
blogs: IBlog[]
}
const BlogCardList: NextPage<Props> = ({blogs}) => {
return (
<div>
{blogs.map((blog, i) => {
return (
<div key={i}>
<BlogTitle
title={blog.attributes.title}
isClickable={true}
blogId={blog.id}
/>
</div>
);
})}
</div>
)
}
export default BlogCardList;
Now to show the list, we need to add BlogCardList
to about page.
const Blog: NextPage<Props> = ({blogs}) => {
return (
<BlogCardList blogs={blogs}/>
)
}
Now if we go to /blog
, we should see a list of blog titles.
Displaying A Single Blog Post
Let's start creating the individual blog post page, create a new file, and call it [id].tsx
inside the blog directory inside the pages directory, initialize a new basic react component.
import {NextPage} from "next";
import {IBlog} from "@/common/types/BlogInterfaces";
interface Props {
blog: IBlog
}
const client = new ApolloClient({
uri: process.env.STRAPI_ADDRESS,
cache: new InMemoryCache()
});
const Blog: NextPage<Props> = ({blog}) => {
return (
<p>Blog Here<p/>
)
}
export default Blog;
We are going to utilize NextJS's getStaticPaths
and getStaticProps
functions, more info can be found here.
export const getStaticPaths: GetStaticPaths = async () => {
const {data} = await client.query({query: LIST_ID })
const ids: IBlogIdentification[] = data.blogPosts.data;
const paths = ids.map(id => {
return {params: {...id}}
})
return {
paths,
fallback: true
}
}
export const getStaticProps: GetStaticProps<Props> = async ({params}) => {
console.log()
const {data} = await client.query({query: SINGLE_BLOG, variables: {blogId: params!.id}})
const blog: IBlog = data.blogPosts.data[0];
return {
props: {
blog
}
}
}
And now, we can access the blog data inside the component.
const Blog: NextPage<Props> = ({blog}) => {
console.log(blog)
return (
<div>
<p>{blog.id}</p>
<p>{blog.attributes.title}</p>
<p>{blog.attributes.description}</p>
<p>{blog.attributes.body}</p>
</div>
)
}
Now our blog is functional; we can retrieve and display a list of blogs and a single blog post.
Top comments (2)
Great article, you got my follow, keep writing!
The type of $blogPost in the SINGLE POST query should be ID! instead of string.