Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project --quickstart
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:
Next.js is an open-source Reactfront-end development web framework that enables you to leverage such features as server-side rendering or generating static websites for React-based web applications.
You may want to directly try the result of this tutorial. Well, we made a starter out of it so give it a try!
You may want to see what the final result of this tutorial looks like live. Well, we made a starter out of it, so feel free to try it out!
The goal here is to be able to create a blog website using Strapi for the back-end and Next.js for the front-end.
This tutorial uses Strapi v4 You need to have node v.12 installed and that's all.
mkdir blog-strapi && cd blog-strapi
This is the easiest part of this tutorial and it’s all thanks to our expansion team who developed a series of Strapi templates that you can use for some different use case.
These templates are Strapi applications that contain pre-built collection-types and single-types suited for the appropriate use case and data type. In this tutorial, you'll use the Blog template and connect a Next.js application to it.
# 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 start to create our Next.js application now in order to fetch our content from Strapi.
Ok ok wait, let's talk about this amazing 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 time, too much time. Therefore we decided to develop templates that are always created with the latest versions of Strapi. This was achieved quite simply by passing the parameter of the repository 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 the purposes of this tutorial, this initial setup should be enough.
Nice! Now that Strapi is ready, you are going to create your Next.js application.
Well, the easiest part has been completed, let's get our hands dirty developing our blog!
1. Next setup
frontend
app by running the following command:npx create-next-app frontend
Once the installation is finished, add the following packages which you’ll need later: moment
, react-moment
, and react-markdown
. These packages allow you to display the publication date in a nice format and the content of articles in Markdown.
You will also need qs
package. A querystring parsing and stringifying library with some added security for easily fetch the data in Strapi.
frontend
folder:yarn add qs moment react-moment react-markdown
Great! You’re all ready now
2. Fetch data from Strapi
First, you need to create a function to fetch data from your Strapi API.
./frontend/lib/api.js
file containing the following code:import qs from "qs";
/**
* Get full Strapi URL from path
* @param {string} path Path of the URL
* @returns {string} Full Strapi URL
*/
export function getStrapiURL(path = "") {
return `${
process.env.NEXT_PUBLIC_STRAPI_API_URL || "http://localhost:1337"
}${path}`;
}
/**
* Helper to make GET requests to Strapi API endpoints
* @param {string} path Path of the API route
* @param {Object} urlParamsObject URL params object, will be stringified
* @param {Object} options Options passed to fetch
* @returns Parsed API call response
*/
export async function fetchAPI(path, urlParamsObject = {}, options = {}) {
// Merge default and user options
const mergedOptions = {
headers: {
"Content-Type": "application/json",
},
...options,
};
// Build request URL
const queryString = qs.stringify(urlParamsObject);
const requestUrl = `${getStrapiURL(
`/api${path}${queryString ? `?${queryString}` : ""}`
)}`;
// Trigger API call
const response = await fetch(requestUrl, mergedOptions);
// Handle response
if (!response.ok) {
console.error(response.statusText);
throw new Error(`An error occured please try again`);
}
const data = await response.json();
return data;
}
You can see two functions here:
fetStrapiURL
: By default, this function will look at a NEXT_PUBLIC_STRAPI_API_URL
env variable in your .env
file. If you don't have one, it will use the default url of Strapi on your machine.
fetchAPI
: This function gets the request URL thanks to the getStrapiURL function above.
Then it calls the fetch function on this requestUrl with some parameters that are stringifyed and returns the data in a JSON format.
You also need to create another function outside of this file in order to fetch Strapi media
./frontend/lib/media.js
file containing the following:import { getStrapiURL } from "./api";
export function getStrapiMedia(media) {
const { url } = media.data.attributes;
const imageUrl = url.startsWith("/") ? getStrapiURL(url) : url;
return imageUrl;
}
This function returns the correct URL of an image depending on where it is hosted (either on your local machine or hosted on a server).
Locally, an image has a URL structure like so: /uploads/… Whereas on Cloudinary it has a URL structure like this: https://cloudinary.com/....
Also, let's add the following code in your next.config.js
file in order to access a remote image, but still use the built-in Next.js Image Optimization API.
module.exports = {
reactStrictMode: true,
images: {
loader: "default",
domains: ["localhost"],
},
};
Your application can now fetch content from your Strapi API and fetch Strapi media.
3. A little bit of css
We don't want to bother you too much with styling in this tutorial, so we created a CSS file that is used throughout all our blog tutorials. Whether you do the Next.js, Gatsby, or Nuxt.js tutorial, you will always have the same frontend result.
./frontend/assets/css/style.css
file containing the following:a {
text-decoration: none;
}
h1 {
font-family: Staatliches;
font-size: 120px;
}
#category {
font-family: Staatliches;
font-weight: 500;
}
#title {
letter-spacing: 0.4px;
font-size: 22px;
font-size: 1.375rem;
line-height: 1.13636;
}
#banner {
margin: 20px;
height: 800px;
}
#editor {
font-size: 16px;
font-size: 1rem;
line-height: 1.75;
}
.uk-navbar-container {
background: #fff !important;
font-family: Staatliches;
}
img:hover {
opacity: 1;
transition: opacity 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
}
4. _app.js
Let's create some pages now!
First, you need to override the default ./frontend/pages/_app.js
in order to add Uikit, the Staatliches font, but more importantly, you need to fetch the Global site settings which are basically the Global single-type in Strapi.
./frontend/pages/_app.js
file with the following:import App from "next/app";
import Head from "next/head";
import "../assets/css/style.css";
import { createContext } from "react";
import { fetchAPI } from "../lib/api";
import { getStrapiMedia } from "../lib/media";
// Store Strapi Global object in context
export const GlobalContext = createContext({});
const MyApp = ({ Component, pageProps }) => {
const { global } = pageProps;
return (
<>
<Head>
<link
rel="shortcut icon"
href={getStrapiMedia(global.attributes.favicon)}
/>
</Head>
<GlobalContext.Provider value={global.attributes}>
<Component {...pageProps} />
</GlobalContext.Provider>
</>
);
};
// getInitialProps disables automatic static optimization for pages that don't
// have getStaticProps. So article, category and home pages still get SSG.
// Hopefully we can replace this with getStaticProps once this issue is fixed:
// https://github.com/vercel/next.js/discussions/10949
MyApp.getInitialProps = async (ctx) => {
// Calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(ctx);
// Fetch global site settings from Strapi
const globalRes = await fetchAPI("/global", {
populate: {
favicon: "*",
defaultSeo: {
populate: "*",
},
},
});
// Pass the data to our page via props
return { ...appProps, pageProps: { global: globalRes.data } };
};
export default MyApp;
You can see here that the fetchAPI function created earlier is used in order to fetch the Global single-type in Strapi. Then it gets stored in a context.
Let's override the _document.js
file to include UIkit and the Staatliches font in our app
./pages/_document.js
file with the following content:import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html>
<Head>
{/* eslint-disable-next-line */}
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Staatliches"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/uikit@3.10.1/dist/css/uikit.min.css"
/>
<script
async
src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.2.0/js/uikit.min.js"
/>
<script
async
src="https://cdn.jsdelivr.net/npm/uikit@3.10.1/dist/js/uikit-icons.min.js"
/>
<script
async
src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.2.0/js/uikit.js"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
5. Components! Lot of them!
Before going any further with the pages, you first need to create some components that will be necessary to build them out. For this tutorial, you need the following components:
Card component
Create a ./frontend/components
folder.
./frontend/components/nav.js
component containing the following:import React from "react";
import Link from "next/link";
const Nav = ({ categories }) => {
return (
<div>
<nav className="uk-navbar-container" data-uk-navbar>
<div className="uk-navbar-left">
<ul className="uk-navbar-nav">
<li>
<Link href="/">
<a>Strapi Blog</a>
</Link>
</li>
</ul>
</div>
<div className="uk-navbar-right">
<ul className="uk-navbar-nav">
{categories.map((category) => {
return (
<li key={category.id}>
<Link href={`/category/${category.attributes.slug}`}>
<a className="uk-link-reset">{category.attributes.name}</a>
</Link>
</li>
);
})}
</ul>
</div>
</nav>
</div>
);
};
export default Nav;
You can see here that this component receives some categories as a prop. Don't forget that when you declare it.
./frontend/components/layout.js
component containing the following:import Nav from "./nav";
const Layout = ({ children, categories, seo }) => (
<>
<Nav categories={categories} />
{children}
</>
);
export default Layout;
This component calls the nav component.
./frontend/components/seo.js
component containing the following:import Head from "next/head";
import { useContext } from "react";
import { GlobalContext } from "../pages/_app";
import { getStrapiMedia } from "../lib/media";
const Seo = ({ seo }) => {
const { defaultSeo, siteName } = useContext(GlobalContext);
const seoWithDefaults = {
...defaultSeo,
...seo,
};
const fullSeo = {
...seoWithDefaults,
// Add title suffix
metaTitle: `${seoWithDefaults.metaTitle} | ${siteName}`,
// Get full image URL
shareImage: getStrapiMedia(seoWithDefaults.shareImage),
};
return (
<Head>
{fullSeo.metaTitle && (
<>
<title>{fullSeo.metaTitle}</title>
<meta property="og:title" content={fullSeo.metaTitle} />
<meta name="twitter:title" content={fullSeo.metaTitle} />
</>
)}
{fullSeo.metaDescription && (
<>
<meta name="description" content={fullSeo.metaDescription} />
<meta property="og:description" content={fullSeo.metaDescription} />
<meta name="twitter:description" content={fullSeo.metaDescription} />
</>
)}
{fullSeo.shareImage && (
<>
<meta property="og:image" content={fullSeo.shareImage} />
<meta name="twitter:image" content={fullSeo.shareImage} />
<meta name="image" content={fullSeo.shareImage} />
</>
)}
{fullSeo.article && <meta property="og:type" content="article" />}
<meta name="twitter:card" content="summary_large_image" />
</Head>
);
};
export default Seo;
This component creates the necessary meta tags for the SEO of your blog.
It uses the GlobalContext created in the ./frontends/pages/_app.js
file and the getStrapiMedia function created earlier to fetch the correct default image for your SEO.
./frontend/components/image.js
component containing the following:import { getStrapiMedia } from "../lib/media";
import NextImage from "next/image";
const Image = ({ image }) => {
const { alternativeText, width, height } = image.data.attributes;
return (
<NextImage
layout="responsive"
width={width}
height={height}
objectFit="contain"
src={getStrapiMedia(image)}
alt={alternativeText || ""}
/>
);
};
export default Image;
Useful component for displaying images.
Stick with me, only 2 components left to create!
./frontend/components/articles.js
component containing the following:import React from "react";
import Card from "./card";
const Articles = ({ articles }) => {
const leftArticlesCount = Math.ceil(articles.length / 5);
const leftArticles = articles.slice(0, leftArticlesCount);
const rightArticles = articles.slice(leftArticlesCount, articles.length);
return (
<div>
<div className="uk-child-width-1-2@s" data-uk-grid="true">
<div>
{leftArticles.map((article, i) => {
return (
<Card
article={article}
key={`article__left__${article.attributes.slug}`}
/>
);
})}
</div>
<div>
<div className="uk-child-width-1-2@m uk-grid-match" data-uk-grid>
{rightArticles.map((article, i) => {
return (
<Card
article={article}
key={`article__left__${article.attributes.slug}`}
/>
);
})}
</div>
</div>
</div>
</div>
);
};
export default Articles;
This component splits your list of articles (for styling purposes) on the right and left-hand sides of your blog. As you can see, it calls a component that I promise will be the last one you need to create!
./frontend/components/card.js
component containing the following:import React from "react";
import Link from "next/link";
import NextImage from "./image";
const Card = ({ article }) => {
return (
<Link href={`/article/${article.attributes.slug}`}>
<a className="uk-link-reset">
<div className="uk-card uk-card-muted">
<div className="uk-card-media-top">
<NextImage image={article.attributes.image} />
</div>
<div className="uk-card-body">
<p id="category" className="uk-text-uppercase">
{article.attributes.category.data.attributes.name}
</p>
<p id="title" className="uk-text-large">
{article.attributes.title}
</p>
</div>
</div>
</a>
</Link>
);
};
export default Card;
Just a simple component that will be used for displaying the article on the main page. It is using the Image component we just created earlier! And we're done for the components!
You’re now ready to create your pages!
6. Pages
First thing to do is to override the default ./frontend/pages/index.js
.
import React from "react";
import Articles from "../components/articles";
import Layout from "../components/layout";
import Seo from "../components/seo";
import { fetchAPI } from "../lib/api";
const Home = ({ articles, categories, homepage }) => {
return (
<Layout categories={categories}>
<Seo seo={homepage.attributes.seo} />
<div className="uk-section">
<div className="uk-container uk-container-large">
<h1>{homepage.attributes.hero.title}</h1>
<Articles articles={articles} />
</div>
</div>
</Layout>
);
};
export async function getStaticProps() {
// Run API calls in parallel
const [articlesRes, categoriesRes, homepageRes] = await Promise.all([
fetchAPI("/articles", { populate: ["image", "category"] }),
fetchAPI("/categories", { populate: "*" }),
fetchAPI("/homepage", {
populate: {
hero: "*",
seo: { populate: "*" },
},
}),
]);
return {
props: {
articles: articlesRes.data,
categories: categoriesRes.data,
homepage: homepageRes.data,
},
revalidate: 1,
};
}
export default Home;
As you can see here, the application fetches the Homepage single-type as well as the Category and Article collection-types. It then displays all of that using the components you just created.
Let's explain what happens for the hommepage
call:
fetchAPI("/homepage", {
populate: {
hero: "*",
seo: { populate: "*" },
},
}),
By default on Strapi v4, the REST API is no longer populating components, relations and dynamic zones for performance purpose (only fetch the minimum and specify what you need). If you wan to fetch them, you'll need to use the populate param. Here we say: for the homepage single-type we want to populate the hero component and get all its fields ("*"
) but also the seo component. You can see that you can specify it in different ways.
Imagine that the hero component has a component inside and that you want to fetch it also you should write something like this:
fetchAPI("/homepage", {
populate: {
hero: { populate: { anotherCompo: { populate: "*" } }},
seo: { populate: "*" },
},
}),
Alright, enough suspense! You can now launch your Next.js app if you haven’t already or simply reload it using the following command:
yarn dev
Now, you need to create a page for each one of your articles.
./frontend/pages/article
folder../frontend/pages/article/[slug].js
file containing the following:import Moment from "react-moment";
import ReactMarkdown from "react-markdown";
import Seo from "../../components/seo";
import Layout from "../../components/layout";
import { fetchAPI } from "../../lib/api";
import { getStrapiMedia } from "../../lib/media";
const Article = ({ article, categories }) => {
const imageUrl = getStrapiMedia(article.attributes.image);
const seo = {
metaTitle: article.attributes.title,
metaDescription: article.attributes.description,
shareImage: article.attributes.image,
article: true,
};
return (
<Layout categories={categories.data}>
<Seo seo={seo} />
<div
id="banner"
className="uk-height-medium uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light uk-padding uk-margin"
data-src={imageUrl}
data-srcset={imageUrl}
data-uk-img
>
<h1>{article.attributes.title}</h1>
</div>
<div className="uk-section">
<div className="uk-container uk-container-small">
<ReactMarkdown children={article.attributes.content} />
<hr className="uk-divider-small" />
<div className="uk-grid-small uk-flex-left" data-uk-grid="true">
<div>
{article.attributes.author.data.attributes.picture && (
<img
src={getStrapiMedia(
article.attributes.author.data.attributes.picture
)}
alt={
article.attributes.author.data.attributes.picture.data
.attributes.alternativeText
}
style={{
position: "static",
borderRadius: "20%",
height: 60,
}}
/>
)}
</div>
<div className="uk-width-expand">
<p className="uk-margin-remove-bottom">
By {article.attributes.author.data.attributes.name}
</p>
<p className="uk-text-meta uk-margin-remove-top">
<Moment format="MMM Do YYYY">
{article.attributes.published_at}
</Moment>
</p>
</div>
</div>
</div>
</div>
</Layout>
);
};
export async function getStaticPaths() {
const articlesRes = await fetchAPI("/articles", { fields: ["slug"] });
return {
paths: articlesRes.data.map((article) => ({
params: {
slug: article.attributes.slug,
},
})),
fallback: false,
};
}
export async function getStaticProps({ params }) {
const articlesRes = await fetchAPI("/articles", {
filters: {
slug: params.slug,
},
populate: ["image", "category", "author.picture"],
});
const categoriesRes = await fetchAPI("/categories");
return {
props: { article: articlesRes.data[0], categories: categoriesRes },
revalidate: 1,
};
}
export default Article;
So there’s a lot of information to unpack here. Let’s break it down into separate sections to make it easier to understand. First of all, it declares the image and SEO of the article.
const imageUrl = getStrapiMedia(article.attributes.image);
const seo = {
metaTitle: article.attributes.title,
metaDescription: article.attributes.description,
shareImage: article.attributes.image,
article: true,
};
Since this page has a dynamic route, the article’s path needs to be defined using its slug. You can find more information about pre-rendering all the paths specified by getStaticPaths.
export async function getStaticPaths() {
const articlesRes = await fetchAPI("/articles", { fields: ["slug"] });
return {
paths: articlesRes.data.map((article) => ({
params: {
slug: article.attributes.slug,
},
})),
fallback: false,
};
}
Then you need to define the props using getStaticProps. You need to fetch the date of the article and the categories.
export async function getStaticProps({ params }) {
const articlesRes = await fetchAPI("/articles", {
filters: {
slug: params.slug,
},
populate: ["image", "category", "author.picture"],
});
const categoriesRes = await fetchAPI("/categories");
return {
props: { article: articlesRes.data[0], categories: categoriesRes },
revalidate: 1,
};
}
Now, if you reload your Next.js application and if you click on any article it should be working properly!
You now need to go through the exact same process as you did for the article page.
./frontend/pages/category
folder../frontend/pages/category/[slug].js
file containing the following:import Seo from "../../components/seo";
import Layout from "../../components/layout";
import Articles from "../../components/articles";
import { fetchAPI } from "../../lib/api";
const Category = ({ category, categories }) => {
const seo = {
metaTitle: category.attributes.name,
metaDescription: `All ${category.attributes.name} articles`,
};
return (
<Layout categories={categories.data}>
<Seo seo={seo} />
<div className="uk-section">
<div className="uk-container uk-container-large">
<h1>{category.attributes.name}</h1>
<Articles articles={category.attributes.articles.data} />
</div>
</div>
</Layout>
);
};
export async function getStaticPaths() {
const categoriesRes = await fetchAPI("/categories", { fields: ["slug"] });
return {
paths: categoriesRes.data.map((category) => ({
params: {
slug: category.attributes.slug,
},
})),
fallback: false,
};
}
export async function getStaticProps({ params }) {
const matchingCategories = await fetchAPI("/categories", {
filters: { slug: params.slug },
populate: {
articles: {
populate: "*",
},
},
});
const allCategories = await fetchAPI("/categories");
return {
props: {
category: matchingCategories.data[0],
categories: allCategories,
},
revalidate: 1,
};
}
export default Category;
As you can see, it’s very similar to the article page. This allows you to filter your list of articles on a specific category. Try it out for yourself!
Huge congrats, you successfully completed this tutorial, you can be proud of yourself. I hope you enjoyed it!
Hungry for more?
Contribute and collaborate on educational content for the Strapi Community
Can't wait to see your contribution!
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.
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
Get all the latest Strapi updates, news and events.