Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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, Next.js, Vue, Nuxt.js or Angular.
This one is for the people who want to build a simple static blog with Gatsby 4!
Gatsby is a blazing-fast website framework for React. It allows developers to build React-based websites within minutes. Whether you want to develop a blog or a corporate website, Gatsby will fill your needs.
Because it is based on React, the website pages are never reloaded which makes the generated website super fast. A large set of plugins is available to allow developers to save time coding. For example, plugins exist to get data from any source (Markdown files, CMS, etc.). Gatsby is strongly based on the "node" interface, which is the center of Gatsby's data system.
Created by Kyle Mathews, the project was officially released in July 2017. (As of February 2109,Gatsby is in Gatsby v2 and is now used by many companies and for hundreds of websites.
Strapi is an open-source Headless CMS. It saves weeks of API development time and allows easy long-term content management through a beautiful administration panel anyone can use.
Unlike other CMSs, Strapi is 100% open-source, which means:
You may want to directly try the result of this tutorial. Well, we made a starter out of it so give it a try:
yarn create strapi-starter gatsby-blog gatsby-blog
The goal here is to be able to create a simple static blog website using Strapi as the backend and Gatsby for the frontend The source code is available on GitHub.
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
That's the easiest part of this tutorial thanks to Rémi who developed a series of Strapi templates that you can use for your Blog, E-commerce, or Corporate website project.
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 Gatsby application to it.
Note: for this tutorial, we will use yarn
as your package manager.
yarn create strapi-app backend --template 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 Gatsby 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 the starters and before the 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 were born our starters.
However, Strapi evolves quickly, very quickly and at the time the starters constituted a repository including the backend as well as the frontend. This means that updating the Strapi version on all our starters took time, too much time. We then decided to develop templates that are always created with the latest versions of Strapi. Quite simply by passing in parameter the repository of the desired template like you just did. Also, it gives you a good architecture for your Strapi project.
Feel free to modify all this, however, we will be satisfied with that for the tutorial.
Let's create an API Token for Gatsby to use it in order to consume data from Strapi.
Keep the token somewhere as you will need it for your Gatsby application
Nice! Now that Strapi is ready, you are going to create your Gatsby application.
The easiest part has been completed, let's get our hands dirty developing our blog with Gatsby!
Gatsby setup
First of all, you'll need to install the Gatsby CLI
yarn global add gatsby-cli
frontend
project by running the following command:npx gatsby new frontend https://github.com/gatsbyjs/gatsby-starter-default
Once the installation is completed, you can start your front-end app to make sure everything went ok.
1
2
cd frontend
gatsby develop
First, let's create a .env.development
file containing some environment variables for our Gatsby application.
.env.development
file at the root of your Gatsby application containing the following:1
2
STRAPI_TOKEN=<strapi-api-token-you-created-earlier>
STRAPI_API_URL=http://localhost:1337
Strapi Setup
To connect Gatsby to a new source of data, you have to develop a new source plugin. Fortunately, several source plugins already exist, so one of them should fill your needs.
In this example, we are using Strapi. Obviously, we are going to need a source plugin for Strapi APIs. Good news: we built it for you!
yarn add gatsby-source-strapi@2.0.0-beta.0,
gatsby-plugin-postcss gatsby-transformer-remark
The first plugin is for fetching your data in your Strapi application. The second one provides drop-in support for PostCSS. The third one adds additional fields to the MarkdownRemark GraphQL type including html , excerpt , headings , etc.
The gatsby-source-strapi
plugin needs to be configured.
gatsby-config.js
with the following code:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
})
module.exports = {
plugins: [
"gatsby-plugin-gatsby-cloud",
"gatsby-plugin-postcss",
{
resolve: "gatsby-source-strapi",
options: {
apiURL: process.env.STRAPI_API_URL || "http://localhost:1337",
accessToken: process.env.STRAPI_TOKEN,
collectionTypes: [
{
singularName: "article",
queryParams: {
publicationState:
process.env.GATSBY_IS_PREVIEW === "true" ? "preview" : "live",
populate: {
cover: "*",
blocks: {
populate: "*",
},
},
},
},
{
singularName: "author",
},
{
singularName: "category",
},
],
singleTypes: [
{
singularName: "about",
queryParams: {
populate: {
blocks: {
populate: "*",
},
},
},
},
{
singularName: "global",
queryParams: {
populate: {
favicon: "*",
defaultSeo: {
populate: "*",
},
},
},
},
],
},
},
"gatsby-plugin-image",
"gatsby-plugin-sharp",
"gatsby-transformer-sharp",
"gatsby-transformer-remark",
],
}
What's important here is that we define our STRAPI_API_URL
for the Strapi API: http://localhost:1337
without a trailling slash and the collection types and single types you want to be able to query from Strapi, here for this tutorial: article, category, author, global and about
Alright! Gatsby is now ready to fetch data from Strapi! Let's clean this app and create the necessary components!
Before we can dive in, we have to clean the default Gatsby architecture by removing useless files for our app.
rm src/components/header.js src/components/layout.css src/pages/page-2.js src/pages/using-typescript.tsx src/pages/404.js src/pages/using-ssr.js src/templates/using-dsg.js
Now let's create all the frontend components for our app!
./src/components/seo.js
file with the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import React from "react"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
const Seo = ({ seo = {} }) => {
const { strapiGlobal } = useStaticQuery(graphql`
query {
strapiGlobal {
siteName
favicon {
localFile {
url
}
}
defaultSeo {
metaTitle
metaDescription
shareImage {
localFile {
url
}
}
}
}
}
`)
const { siteName, defaultSeo, favicon } = strapiGlobal
// Merge default and page-specific SEO values
const fullSeo = { ...defaultSeo, ...seo }
// Add site name to title
fullSeo.metaTitle = `${fullSeo.metaTitle} | ${siteName}`
const getMetaTags = () => {
const tags = []
if (fullSeo.metaTitle) {
tags.push(
{
property: "og:title",
content: fullSeo.metaTitle,
},
{
name: "twitter:title",
content: fullSeo.metaTitle,
}
)
}
if (fullSeo.metaDescription) {
tags.push(
{
name: "description",
content: fullSeo.metaDescription,
},
{
property: "og:description",
content: fullSeo.metaDescription,
},
{
name: "twitter:description",
content: fullSeo.metaDescription,
}
)
}
if (fullSeo.shareImage) {
const imageUrl = fullSeo.shareImage.localFile.url
tags.push(
{
name: "image",
content: imageUrl,
},
{
property: "og:image",
content: imageUrl,
},
{
name: "twitter:image",
content: imageUrl,
}
)
}
if (fullSeo.article) {
tags.push({
property: "og:type",
content: "article",
})
}
tags.push({ name: "twitter:card", content: "summary_large_image" })
return tags
}
const metaTags = getMetaTags()
return (
<Helmet
title={fullSeo.metaTitle}
link={[
{
rel: "icon",
href: favicon.localFile.url,
},
]}
meta={metaTags}
/>
)
}
export default Seo
./src/components/layout.js
file with the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from "react"
import Footer from "./footer"
import Navbar from "./navbar"
const Layout = ({ children }) => {
return (
<div class="flex min-h-screen flex-col justify-between bg-neutral-50 text-neutral-900">
<div>
<Navbar />
{children}
</div>
<Footer />
</div>
)
}
export default Layout
This component needs a Navbar and a Footer! Let's create them.
./src/components/navbar.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Link } from "gatsby"
import React from "react"
const Navbar = () => {
return (
<header className="bg-primary-200">
<nav className="container flex flex-row items-baseline justify-between py-6">
<Link to="/" className="text-xl font-medium">
Blog
</Link>
<div className="flex flex-row items-baseline justify-end">
<Link className="font-medium" to="/about">
About
</Link>
</div>
</nav>
</header>
)
}
export default Navbar
./src/components/footer.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react"
const Footer = () => {
const currentYear = new Date().getFullYear()
return (
<footer className="mt-16 bg-neutral-100 py-8 text-neutral-700">
<div className="container">
<p>Copyright {currentYear}</p>
</div>
</footer>
)
}
export default Footer
./src/components/headings.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react"
const Headings = ({ title, description }) => {
return (
<header className="container mt-8">
<h1 className="text-6xl font-bold text-neutral-700">{title}</h1>
{description && (
<p className="mt-4 text-2xl text-neutral-500">{description}</p>
)}
</header>
)
}
export default Headings
./src/components/article-card.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React from "react"
import { Link, graphql } from "gatsby"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
const ArticleCard = ({ article }) => {
return (
<Link
to={`/article/${article.slug}`}
className="overflow-hidden rounded-lg bg-white shadow-sm transition-shadow hover:shadow-md"
>
<GatsbyImage
image={getImage(article.cover?.localFile)}
alt={article.cover?.alternativeText}
/>
<div className="px-4 py-4">
<h3 className="font-bold text-neutral-700">{article.title}</h3>
<p className="line-clamp-2 mt-2 text-neutral-500">
{article.description}
</p>
</div>
</Link>
)
}
export const query = graphql`
fragment ArticleCard on STRAPI_ARTICLE {
id
slug
title
description
cover {
alternativeText
localFile {
childImageSharp {
gatsbyImageData(aspectRatio: 1.77)
}
}
}
}
`
export default ArticleCard
./src/components/articles-grid.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react"
import ArticleCard from "./article-card"
const ArticlesGrid = ({ articles }) => {
return (
<div className="container mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{articles.map((article) => (
<ArticleCard article={article} />
))}
</div>
)
}
export default ArticlesGrid
./src/components/block-media.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
const BlockMedia = ({ data }) => {
const isVideo = data.file.mime.startsWith("video")
return (
<div className="py-8">
{isVideo ? (
<p>TODO video</p>
) : (
<GatsbyImage
image={getImage(data.file.localFile)}
alt={data.file.alternativeText}
/>
)}
</div>
)
}
export default BlockMedia
./src/components/block-quote.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react"
const BlockQuote = ({ data }) => {
return (
<div className="py-6">
<blockquote className="container max-w-xl border-l-4 border-neutral-700 py-2 pl-6 text-neutral-700">
<p className="text-5xl font-medium italic">{data.quoteBody}</p>
<cite className="mt-4 block font-bold uppercase not-italic">
{data.title}
</cite>
</blockquote>
</div>
)
}
export default BlockQuote
./src/components/block-rich-text.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react"
const BlockRichText = ({ data }) => {
return (
<div className="prose mx-auto py-8">
<div
dangerouslySetInnerHTML={{
__html: data.richTextBody.data.childMarkdownRemark.html,
}}
/>
</div>
)
}
export default BlockRichText
./src/components/block-slider.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
import Slider from "react-slick"
import "slick-carousel/slick/slick.css"
import "slick-carousel/slick/slick-theme.css"
const BlockSlider = ({ data }) => {
return (
<div className="container max-w-3xl py-8">
<Slider
dots={true}
infinite={true}
speed={300}
slidesToShow={1}
slidesToScroll={1}
arrows={true}
swipe={true}
>
{data.files.map((file) => (
<GatsbyImage
key={file.id}
image={getImage(file.localFile)}
alt={file.alternativeText}
/>
))}
</Slider>
</div>
)
}
export default BlockSlider
./src/components/blocks-renderer.js
file containing the following content:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import React from "react"
import { graphql } from "gatsby"
import BlockRichText from "./block-rich-text"
import BlockMedia from "./block-media"
import BlockQuote from "./block-quote"
import BlockSlider from "./block-slider"
const componentsMap = {
STRAPI__COMPONENT_SHARED_RICH_TEXT: BlockRichText,
STRAPI__COMPONENT_SHARED_MEDIA: BlockMedia,
STRAPI__COMPONENT_SHARED_QUOTE: BlockQuote,
STRAPI__COMPONENT_SHARED_SLIDER: BlockSlider,
}
const Block = ({ block }) => {
const Component = componentsMap[block.__typename]
if (!Component) {
return null
}
return <Component data={block} />
}
const BlocksRenderer = ({ blocks }) => {
return (
<div>
{blocks.map((block, index) => (
<Block key={`${index}${block.__typename}`} block={block} />
))}
</div>
)
}
export const query = graphql`
fragment Blocks on STRAPI__COMPONENT_SHARED_MEDIASTRAPI__COMPONENT_SHARED_QUOTESTRAPI__COMPONENT_SHARED_RICH_TEXTSTRAPI__COMPONENT_SHARED_SLIDERUnion {
__typename
... on STRAPI__COMPONENT_SHARED_RICH_TEXT {
richTextBody: body {
__typename
data {
id
childMarkdownRemark {
html
}
}
}
}
... on STRAPI__COMPONENT_SHARED_MEDIA {
file {
mime
localFile {
childImageSharp {
gatsbyImageData
}
}
}
}
... on STRAPI__COMPONENT_SHARED_QUOTE {
title
quoteBody: body
}
... on STRAPI__COMPONENT_SHARED_SLIDER {
files {
id
mime
localFile {
childImageSharp {
gatsbyImageData
}
}
}
}
}
`
export default BlocksRenderer
This component will be used for rendering our Strapi components! Perfect! All our frontend components are ready to be used.
yarn add -D tailwindcss postcss autoprefixer
./tailwind.config.js
file containing the following:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const colors = require("tailwindcss/colors")
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
colors: {
neutral: colors.neutral,
primary: colors.sky,
},
},
container: {
center: true,
padding: {
DEFAULT: "1rem",
xs: "1rem",
sm: "2rem",
xl: "5rem",
"2xl": "6rem",
},
},
},
plugins: [
require("@tailwindcss/line-clamp"),
require("@tailwindcss/typography"),
],
}
postcss.config.js
with the following code:1
2
3
4
5
6
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
./src/styles/global.css
with the following content:1
2
3
@tailwind base;
@tailwind components;
@tailwind utilities;
gatsby-browser.js
file with the following:1
import "./src/styles/global.css"
Great! Tailwind is now installed in this project!
Let's update our ./src/pages/index.js
page with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"
import ArticlesGrid from "../components/articles-grid"
import Seo from "../components/seo"
import Headings from "../components/headings"
const IndexPage = () => {
const { allStrapiArticle, strapiGlobal } = useStaticQuery(graphql`
query {
allStrapiArticle {
nodes {
...ArticleCard
}
}
strapiGlobal {
siteName
siteDescription
}
}
`)
return (
<Layout>
<Seo seo={{ metaTitle: "Home" }} />
<Headings
title={strapiGlobal.siteName}
description={strapiGlobal.siteDescription}
/>
<main>
<ArticlesGrid articles={allStrapiArticle.nodes} />
</main>
</Layout>
)
}
export default IndexPage
Now let's update our ./src/pages/about.js
page with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"
import Seo from "../components/seo"
import BlocksRenderer from "../components/blocks-renderer"
import Headings from "../components/headings"
const AboutPage = () => {
const { strapiAbout } = useStaticQuery(graphql`
query {
strapiAbout {
title
blocks {
...Blocks
}
}
}
`)
const { title, blocks } = strapiAbout
const seo = {
metaTitle: title,
metaDescription: title,
}
return (
<Layout>
<Seo seo={seo} />
<Headings title={strapiAbout.title} />
<BlocksRenderer blocks={blocks} />
</Layout>
)
}
export default AboutPage
Great! Now let's define our template. But before we need to update the gatsby-node.js
file:
gatsby-node.js
file with the following:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const path = require("path")
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
// Define a template for blog post
const articlePost = path.resolve("./src/templates/article-post.js")
const result = await graphql(
`
{
allStrapiArticle {
nodes {
title
slug
}
}
}
`
)
if (result.errors) {
reporter.panicOnBuild(
`There was an error loading your Strapi articles`,
result.errors
)
return
}
const articles = result.data.allStrapiArticle.nodes
if (articles.length > 0) {
articles.forEach((article) => {
createPage({
path: `/article/${article.slug}`,
component: articlePost,
context: {
slug: article.slug,
},
})
})
}
}
This will define a nnew template for displaying blog post pages dynamically. You'll need to create this template in your template folder to do so.
./src/templates/article-post.js
file with the following code:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import React from "react"
import { graphql } from "gatsby"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
import Layout from "../components/layout"
import BlocksRenderer from "../components/blocks-renderer"
import Seo from "../components/seo"
const ArticlePage = ({ data }) => {
const article = data.strapiArticle
const seo = {
metaTitle: article.title,
metaDescription: article.description,
shareImage: article.cover,
}
return (
<Layout as="article">
<Seo seo={seo} />
<header className="container max-w-4xl py-8">
<h1 className="text-6xl font-bold text-neutral-700">{article.title}</h1>
<p className="mt-4 text-2xl text-neutral-500">{article.description}</p>
<GatsbyImage
image={getImage(article?.cover?.localFile)}
alt={article?.cover?.alternativeText}
className="mt-6"
/>
</header>
<main className="mt-8">
<BlocksRenderer blocks={article.blocks || []} />
</main>
</Layout>
)
}
export const pageQuery = graphql`
query ($slug: String) {
strapiArticle(slug: { eq: $slug }) {
id
slug
title
description
blocks {
...Blocks
}
cover {
alternativeText
localFile {
url
childImageSharp {
gatsbyImageData
}
}
}
}
}
`
export default ArticlePage
Perfect! You should be good now.
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 own needs, and give your feedback in the comment section below.
If you want to deploy your application, check our documentation.
Contribute and collaborate on educational content for the Strapi Community https://strapi.io/write-for-the-community
Can't wait to see your contribution!
One last thing, we are trying to make the best possible tutorials for you, help us in this mission by answering this short survey https://strapisolutions.typeform.com/to/bwXvhA?channel=xxxxx
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.
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