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.
1cd frontend
2gatsby 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:1STRAPI_TOKEN=<strapi-api-token-you-created-earlier>
2STRAPI_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:1require("dotenv").config({
2 path: `.env.${process.env.NODE_ENV}`,
3})
4
5module.exports = {
6 plugins: [
7 "gatsby-plugin-gatsby-cloud",
8 "gatsby-plugin-postcss",
9 {
10 resolve: "gatsby-source-strapi",
11 options: {
12 apiURL: process.env.STRAPI_API_URL || "http://localhost:1337",
13 accessToken: process.env.STRAPI_TOKEN,
14 collectionTypes: [
15 {
16 singularName: "article",
17 queryParams: {
18 publicationState:
19 process.env.GATSBY_IS_PREVIEW === "true" ? "preview" : "live",
20 populate: {
21 cover: "*",
22 blocks: {
23 populate: "*",
24 },
25 },
26 },
27 },
28 {
29 singularName: "author",
30 },
31 {
32 singularName: "category",
33 },
34 ],
35 singleTypes: [
36 {
37 singularName: "about",
38 queryParams: {
39 populate: {
40 blocks: {
41 populate: "*",
42 },
43 },
44 },
45 },
46 {
47 singularName: "global",
48 queryParams: {
49 populate: {
50 favicon: "*",
51 defaultSeo: {
52 populate: "*",
53 },
54 },
55 },
56 },
57 ],
58 },
59 },
60 "gatsby-plugin-image",
61 "gatsby-plugin-sharp",
62 "gatsby-transformer-sharp",
63 "gatsby-transformer-remark",
64 ],
65}
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:1import React from "react"
2import { Helmet } from "react-helmet"
3import { useStaticQuery, graphql } from "gatsby"
4
5const Seo = ({ seo = {} }) => {
6 const { strapiGlobal } = useStaticQuery(graphql`
7 query {
8 strapiGlobal {
9 siteName
10 favicon {
11 localFile {
12 url
13 }
14 }
15 defaultSeo {
16 metaTitle
17 metaDescription
18 shareImage {
19 localFile {
20 url
21 }
22 }
23 }
24 }
25 }
26 `)
27
28 const { siteName, defaultSeo, favicon } = strapiGlobal
29
30 // Merge default and page-specific SEO values
31 const fullSeo = { ...defaultSeo, ...seo }
32
33 // Add site name to title
34 fullSeo.metaTitle = `${fullSeo.metaTitle} | ${siteName}`
35
36 const getMetaTags = () => {
37 const tags = []
38
39 if (fullSeo.metaTitle) {
40 tags.push(
41 {
42 property: "og:title",
43 content: fullSeo.metaTitle,
44 },
45 {
46 name: "twitter:title",
47 content: fullSeo.metaTitle,
48 }
49 )
50 }
51 if (fullSeo.metaDescription) {
52 tags.push(
53 {
54 name: "description",
55 content: fullSeo.metaDescription,
56 },
57 {
58 property: "og:description",
59 content: fullSeo.metaDescription,
60 },
61 {
62 name: "twitter:description",
63 content: fullSeo.metaDescription,
64 }
65 )
66 }
67 if (fullSeo.shareImage) {
68 const imageUrl = fullSeo.shareImage.localFile.url
69 tags.push(
70 {
71 name: "image",
72 content: imageUrl,
73 },
74 {
75 property: "og:image",
76 content: imageUrl,
77 },
78 {
79 name: "twitter:image",
80 content: imageUrl,
81 }
82 )
83 }
84 if (fullSeo.article) {
85 tags.push({
86 property: "og:type",
87 content: "article",
88 })
89 }
90 tags.push({ name: "twitter:card", content: "summary_large_image" })
91
92 return tags
93 }
94
95 const metaTags = getMetaTags()
96
97 return (
98 <Helmet
99 title={fullSeo.metaTitle}
100 link={[
101 {
102 rel: "icon",
103 href: favicon.localFile.url,
104 },
105 ]}
106 meta={metaTags}
107 />
108 )
109}
110
111export default Seo
./src/components/layout.js
file with the following content:1import React from "react"
2import Footer from "./footer"
3import Navbar from "./navbar"
4
5const Layout = ({ children }) => {
6 return (
7 <div class="flex min-h-screen flex-col justify-between bg-neutral-50 text-neutral-900">
8 <div>
9 <Navbar />
10 {children}
11 </div>
12 <Footer />
13 </div>
14 )
15}
16
17export default Layout
This component needs a Navbar and a Footer! Let's create them.
./src/components/navbar.js
file containing the following content:1import { Link } from "gatsby"
2import React from "react"
3
4const Navbar = () => {
5 return (
6 <header className="bg-primary-200">
7 <nav className="container flex flex-row items-baseline justify-between py-6">
8 <Link to="/" className="text-xl font-medium">
9 Blog
10 </Link>
11 <div className="flex flex-row items-baseline justify-end">
12 <Link className="font-medium" to="/about">
13 About
14 </Link>
15 </div>
16 </nav>
17 </header>
18 )
19}
20
21export default Navbar
./src/components/footer.js
file containing the following content:1import React from "react"
2
3const Footer = () => {
4 const currentYear = new Date().getFullYear()
5
6 return (
7 <footer className="mt-16 bg-neutral-100 py-8 text-neutral-700">
8 <div className="container">
9 <p>Copyright {currentYear}</p>
10 </div>
11 </footer>
12 )
13}
14
15export default Footer
./src/components/headings.js
file containing the following content:1import React from "react"
2
3const Headings = ({ title, description }) => {
4 return (
5 <header className="container mt-8">
6 <h1 className="text-6xl font-bold text-neutral-700">{title}</h1>
7 {description && (
8 <p className="mt-4 text-2xl text-neutral-500">{description}</p>
9 )}
10 </header>
11 )
12}
13
14export default Headings
./src/components/article-card.js
file containing the following content:1import React from "react"
2import { Link, graphql } from "gatsby"
3import { GatsbyImage, getImage } from "gatsby-plugin-image"
4
5const ArticleCard = ({ article }) => {
6 return (
7 <Link
8 to={`/article/${article.slug}`}
9 className="overflow-hidden rounded-lg bg-white shadow-sm transition-shadow hover:shadow-md"
10 >
11 <GatsbyImage
12 image={getImage(article.cover?.localFile)}
13 alt={article.cover?.alternativeText}
14 />
15 <div className="px-4 py-4">
16 <h3 className="font-bold text-neutral-700">{article.title}</h3>
17 <p className="line-clamp-2 mt-2 text-neutral-500">
18 {article.description}
19 </p>
20 </div>
21 </Link>
22 )
23}
24
25export const query = graphql`
26 fragment ArticleCard on STRAPI_ARTICLE {
27 id
28 slug
29 title
30 description
31 cover {
32 alternativeText
33 localFile {
34 childImageSharp {
35 gatsbyImageData(aspectRatio: 1.77)
36 }
37 }
38 }
39 }
40`
41
42export default ArticleCard
./src/components/articles-grid.js
file containing the following content:1import React from "react"
2import ArticleCard from "./article-card"
3
4const ArticlesGrid = ({ articles }) => {
5 return (
6 <div className="container mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
7 {articles.map((article) => (
8 <ArticleCard article={article} />
9 ))}
10 </div>
11 )
12}
13
14export default ArticlesGrid
./src/components/block-media.js
file containing the following content:1import React from "react"
2import { GatsbyImage, getImage } from "gatsby-plugin-image"
3
4const BlockMedia = ({ data }) => {
5 const isVideo = data.file.mime.startsWith("video")
6
7 return (
8 <div className="py-8">
9 {isVideo ? (
10 <p>TODO video</p>
11 ) : (
12 <GatsbyImage
13 image={getImage(data.file.localFile)}
14 alt={data.file.alternativeText}
15 />
16 )}
17 </div>
18 )
19}
20
21export default BlockMedia
./src/components/block-quote.js
file containing the following content:1import React from "react"
2
3const BlockQuote = ({ data }) => {
4 return (
5 <div className="py-6">
6 <blockquote className="container max-w-xl border-l-4 border-neutral-700 py-2 pl-6 text-neutral-700">
7 <p className="text-5xl font-medium italic">{data.quoteBody}</p>
8 <cite className="mt-4 block font-bold uppercase not-italic">
9 {data.title}
10 </cite>
11 </blockquote>
12 </div>
13 )
14}
15
16export default BlockQuote
./src/components/block-rich-text.js
file containing the following content:1import React from "react"
2
3const BlockRichText = ({ data }) => {
4 return (
5 <div className="prose mx-auto py-8">
6 <div
7 dangerouslySetInnerHTML={{
8 __html: data.richTextBody.data.childMarkdownRemark.html,
9 }}
10 />
11 </div>
12 )
13}
14
15export default BlockRichText
./src/components/block-slider.js
file containing the following content:1import React from "react"
2import { GatsbyImage, getImage } from "gatsby-plugin-image"
3import Slider from "react-slick"
4import "slick-carousel/slick/slick.css"
5import "slick-carousel/slick/slick-theme.css"
6
7const BlockSlider = ({ data }) => {
8 return (
9 <div className="container max-w-3xl py-8">
10 <Slider
11 dots={true}
12 infinite={true}
13 speed={300}
14 slidesToShow={1}
15 slidesToScroll={1}
16 arrows={true}
17 swipe={true}
18 >
19 {data.files.map((file) => (
20 <GatsbyImage
21 key={file.id}
22 image={getImage(file.localFile)}
23 alt={file.alternativeText}
24 />
25 ))}
26 </Slider>
27 </div>
28 )
29}
30
31export default BlockSlider
./src/components/blocks-renderer.js
file containing the following content:1import React from "react"
2import { graphql } from "gatsby"
3import BlockRichText from "./block-rich-text"
4import BlockMedia from "./block-media"
5import BlockQuote from "./block-quote"
6import BlockSlider from "./block-slider"
7
8const componentsMap = {
9 STRAPI__COMPONENT_SHARED_RICH_TEXT: BlockRichText,
10 STRAPI__COMPONENT_SHARED_MEDIA: BlockMedia,
11 STRAPI__COMPONENT_SHARED_QUOTE: BlockQuote,
12 STRAPI__COMPONENT_SHARED_SLIDER: BlockSlider,
13}
14
15const Block = ({ block }) => {
16 const Component = componentsMap[block.__typename]
17
18 if (!Component) {
19 return null
20 }
21
22 return <Component data={block} />
23}
24
25const BlocksRenderer = ({ blocks }) => {
26 return (
27 <div>
28 {blocks.map((block, index) => (
29 <Block key={`${index}${block.__typename}`} block={block} />
30 ))}
31 </div>
32 )
33}
34
35export const query = graphql`
36 fragment Blocks on STRAPI__COMPONENT_SHARED_MEDIASTRAPI__COMPONENT_SHARED_QUOTESTRAPI__COMPONENT_SHARED_RICH_TEXTSTRAPI__COMPONENT_SHARED_SLIDERUnion {
37 __typename
38 ... on STRAPI__COMPONENT_SHARED_RICH_TEXT {
39 richTextBody: body {
40 __typename
41 data {
42 id
43 childMarkdownRemark {
44 html
45 }
46 }
47 }
48 }
49 ... on STRAPI__COMPONENT_SHARED_MEDIA {
50 file {
51 mime
52 localFile {
53 childImageSharp {
54 gatsbyImageData
55 }
56 }
57 }
58 }
59 ... on STRAPI__COMPONENT_SHARED_QUOTE {
60 title
61 quoteBody: body
62 }
63 ... on STRAPI__COMPONENT_SHARED_SLIDER {
64 files {
65 id
66 mime
67 localFile {
68 childImageSharp {
69 gatsbyImageData
70 }
71 }
72 }
73 }
74 }
75`
76
77export 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:1const colors = require("tailwindcss/colors")
2
3module.exports = {
4 content: ["./src/**/*.{js,jsx,ts,tsx}"],
5 theme: {
6 extend: {
7 colors: {
8 neutral: colors.neutral,
9 primary: colors.sky,
10 },
11 },
12 container: {
13 center: true,
14 padding: {
15 DEFAULT: "1rem",
16 xs: "1rem",
17 sm: "2rem",
18 xl: "5rem",
19 "2xl": "6rem",
20 },
21 },
22 },
23 plugins: [
24 require("@tailwindcss/line-clamp"),
25 require("@tailwindcss/typography"),
26 ],
27}
postcss.config.js
with the following code:1module.exports = {
2 plugins: {
3 tailwindcss: {},
4 autoprefixer: {},
5 },
6}
./src/styles/global.css
with the following content:1@tailwind base;
2@tailwind components;
3@tailwind utilities;
gatsby-browser.js
file with the following:1import "./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:
1import React from "react"
2import { useStaticQuery, graphql } from "gatsby"
3import Layout from "../components/layout"
4import ArticlesGrid from "../components/articles-grid"
5import Seo from "../components/seo"
6import Headings from "../components/headings"
7
8const IndexPage = () => {
9 const { allStrapiArticle, strapiGlobal } = useStaticQuery(graphql`
10 query {
11 allStrapiArticle {
12 nodes {
13 ...ArticleCard
14 }
15 }
16 strapiGlobal {
17 siteName
18 siteDescription
19 }
20 }
21 `)
22
23 return (
24 <Layout>
25 <Seo seo={{ metaTitle: "Home" }} />
26 <Headings
27 title={strapiGlobal.siteName}
28 description={strapiGlobal.siteDescription}
29 />
30 <main>
31 <ArticlesGrid articles={allStrapiArticle.nodes} />
32 </main>
33 </Layout>
34 )
35}
36
37export default IndexPage
Now let's update our ./src/pages/about.js
page with the following content:
1import React from "react"
2import { useStaticQuery, graphql } from "gatsby"
3import Layout from "../components/layout"
4import Seo from "../components/seo"
5import BlocksRenderer from "../components/blocks-renderer"
6import Headings from "../components/headings"
7
8const AboutPage = () => {
9 const { strapiAbout } = useStaticQuery(graphql`
10 query {
11 strapiAbout {
12 title
13 blocks {
14 ...Blocks
15 }
16 }
17 }
18 `)
19 const { title, blocks } = strapiAbout
20
21 const seo = {
22 metaTitle: title,
23 metaDescription: title,
24 }
25
26 return (
27 <Layout>
28 <Seo seo={seo} />
29 <Headings title={strapiAbout.title} />
30 <BlocksRenderer blocks={blocks} />
31 </Layout>
32 )
33}
34
35export 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:1const path = require("path")
2
3exports.createPages = async ({ graphql, actions, reporter }) => {
4 const { createPage } = actions
5
6 // Define a template for blog post
7 const articlePost = path.resolve("./src/templates/article-post.js")
8
9 const result = await graphql(
10 `
11 {
12 allStrapiArticle {
13 nodes {
14 title
15 slug
16 }
17 }
18 }
19 `
20 )
21
22 if (result.errors) {
23 reporter.panicOnBuild(
24 `There was an error loading your Strapi articles`,
25 result.errors
26 )
27
28 return
29 }
30
31 const articles = result.data.allStrapiArticle.nodes
32
33 if (articles.length > 0) {
34 articles.forEach((article) => {
35 createPage({
36 path: `/article/${article.slug}`,
37 component: articlePost,
38 context: {
39 slug: article.slug,
40 },
41 })
42 })
43 }
44}
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:1import React from "react"
2import { graphql } from "gatsby"
3import { GatsbyImage, getImage } from "gatsby-plugin-image"
4import Layout from "../components/layout"
5import BlocksRenderer from "../components/blocks-renderer"
6import Seo from "../components/seo"
7
8const ArticlePage = ({ data }) => {
9 const article = data.strapiArticle
10
11 const seo = {
12 metaTitle: article.title,
13 metaDescription: article.description,
14 shareImage: article.cover,
15 }
16
17 return (
18 <Layout as="article">
19 <Seo seo={seo} />
20 <header className="container max-w-4xl py-8">
21 <h1 className="text-6xl font-bold text-neutral-700">{article.title}</h1>
22 <p className="mt-4 text-2xl text-neutral-500">{article.description}</p>
23 <GatsbyImage
24 image={getImage(article?.cover?.localFile)}
25 alt={article?.cover?.alternativeText}
26 className="mt-6"
27 />
28 </header>
29 <main className="mt-8">
30 <BlocksRenderer blocks={article.blocks || []} />
31 </main>
32 </Layout>
33 )
34}
35
36export const pageQuery = graphql`
37 query ($slug: String) {
38 strapiArticle(slug: { eq: $slug }) {
39 id
40 slug
41 title
42 description
43 blocks {
44 ...Blocks
45 }
46 cover {
47 alternativeText
48 localFile {
49 url
50 childImageSharp {
51 gatsbyImageData
52 }
53 }
54 }
55 }
56 }
57`
58
59export 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