使用Strapi和Gatsby搭建一个blog网站

文章目录
  1. 安装Strapi
  2. 安装Gatsby
  3. 配置UIKit前端框架
  4. 配置gatsby数据接口
  5. 准备后端数据
  6. 前端数据的展示

写在前面

最近研究了一下比较火Headless CMS,中文应该叫无头CMS?一段时间研究下来觉得这种产品设计理念非常先进,绝对会成为未来的主流,实际上ProcessWire CMS也有点相像,只是PW门槛更低,后台的业务内容和设计都是自己完成外它还提供了一套非常简单易用的Api和前端模板系统搭配使用,这是我最喜欢的这一点,在整个CMS框架体系下开发者非常自由,这是传统的CMS无法比拟的,无头CMS使我们回归到内容管理的本源,完全打破了设计优先的传统理念。

关于Strapi

Strapi是我最早接触的Headless CMS之一,18年的时候第一次接触了Strapi在本地搭建了一个实例,打开后台就对它一见钟情:在它身上看到了很多PW的影子,非常简洁但是又强大,但是由于当时是alpha版本所以仅作为学习研究使用,并没有用于生产环境。

喜欢Strapi的几个特性:

  • 对多数据库的支持:sqlite3、mysql、postgresql、mongodb基本全部支持,PW只只是MYSQL
  • 完全以内容优先,只生产数据,搭配前端框架(React,Vue)做到完全前后端分离,这种设计模式未来会成为主流
  • 支持graphql
  • nodejs架构
  • 带有简易清爽的后台系统

当然它还有更多特点这里不一一叙述了。题外话有点多,下面正式切入正题吧

网上关于strapi+gatsby的实例教程有很多,不管是gatsby还是strapi都提供了入门的blog系统的搭建,但是实际上新手参照教程把整个流程跑下来也未必有几个能成功的,不管是git clone下来还是按照教程一步步来的,本文参考了strapi官网最新发布于2020年2月3日的《Build a static blog with Gatsby and Strapi》这篇教程,但是我也发现了这个内容的几个错误:

  1. seo.js组件没有被合理使用,导致title为空,页面标题没有正确显示
  2. 因为笔者使用了MongoDB作为后端数据库,所有数据记录的ID类型为String,而article.jscategory.jsGraphQL的查询参数类型的声明是Intquery ArticleQuery($id: Int!)
  3. 与2的情况相同,articles.js中调取的article.node.id,应该为article.node.strapiId

以上问题均在本文均被得到修正。

最后说一下我的开发环境吧:Windows10 + NodeJS v13.9.0 + MongoDB v4.1.8 + Gatsby 2.10.9 + Strapi 3.0.0-alpha.24.1

如上所述,如果你的环境和我的开发环境有出入请谨慎直接套用,特别注意上面说到的MongoDB数据库,如果是其它数据库以数字整型Int作为索引的情况肯定是不适用的,要做部分修改。

安装Strapi

创建项目目录

mkdir blog-strapi && cd blog-strapi

安装Strapi,参数说明:

  • --quickstart 是无需配置,使用sqlite3作为数据库快速安装,如果你想用其它数据库可以取消这个参数
  • --no-run 安装完成后不需要启动,因为后面我们还需要安装插件
yarn create strapi-app backend --quickstart --no-run

安装完成后我们来安装一个很重要的graphql插件

yarn strapi install graphql

插件安装完成后用开发模式启动strapi

yarn develop

启动成功

yarn run v1.21.0
$ strapi develop

 Project information

┌────────────────────┬──────────────────────────────────────────────────┐
│ Time               │ Sat Mar 14 2020 22:49:52 GMT+0800 (China Standa… │
│ Launched in        │ 4080 ms                                          │
│ Environment        │ development                                      │
│ Process PID        │ 52656                                            │
│ Version            │ 3.0.0-beta.19.3 (node v13.9.0)                   │
└────────────────────┴──────────────────────────────────────────────────┘

 Actions available

Welcome back!
To manage your project �, go to the administration panel at:
http://localhost:1337/admin

To access the server ⚡️, go to:
http://localhost:1337

首次启动会弹出后台我们只需要按照要求填写账号密码即可。

初始化管理员账号

安装Gatsby

Gatsby在Windows上安装可能坑比较多,比如我几天前就遇到了libpng-dev依赖缺失的问题,但是身为填坑专业户这不算什么。

安装gatsby-cli,如果你已经安装过可以直接忽略此步

npm install -g gatsby-cli

创建gatsby项目

gatsby new frontend

安装完毕后启动gatsby服务

cd frontend
gatsby develop
Gatsby默认页面

配置UIKit前端框架

本示例用了UIKit CSS框架,先需要修改src/components/seo.js文件meta={[]}种添加以下参数引入核心文件

...
link={[  
  {
    rel: "stylesheet",
    href: "https://fonts.googleapis.com/css?family=Staatliches",
  },
  {
    rel: "stylesheet",
    href:
      "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/uikit.min.css",
  },
]}
script={[  
  {
    src:
      "https://cdnjs.cloudflare.com/ajax/libs/uikit/3.2.0/js/uikit.min.js",
  },
  {
    src:
      "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit-icons.min.js",
  },
  {
    src: "https://cdnjs.cloudflare.com/ajax/libs/uikit/3.2.0/js/uikit.js",
  },
]}
...

完整的src/components/seo.js文件

import React from "react"
import PropTypes from "prop-types"
import Helmet from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"

function SEO({ description, lang, meta, title }) {
  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            title
            description
            author
          }
        }
      }
    `
  )

  const metaDescription = description || site.siteMetadata.description

  const metaTitle = title || site.siteMetadata.title

  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}

      title={metaTitle}

      titleTemplate={`%s | ${site.siteMetadata.title}`}
      
      meta={[
        {
          name: `description`,
          content: metaDescription,
        },
        {
          property: `og:title`,
          content: metaTitle,
        },
        {
          property: `og:description`,
          content: metaDescription,
        },
        {
          property: `og:type`,
          content: `website`,
        },
        {
          name: `twitter:card`,
          content: `summary`,
        },
        {
          name: `twitter:creator`,
          content: site.siteMetadata.author,
        },
        {
          name: `twitter:title`,
          content: metaTitle,
        },
        {
          name: `twitter:description`,
          content: metaDescription,
        },
      ].concat(meta)}


      //custom styles
      link={[  
        {
          rel: "stylesheet",
          href: "https://fonts.googleapis.com/css?family=Staatliches",
        },
        {
          rel: "stylesheet",
          href:
            "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/uikit.min.css",
        },
      ]}

      //custom scripts
      script={[  
        {
          src:
            "https://cdnjs.cloudflare.com/ajax/libs/uikit/3.2.0/js/uikit.min.js",
        },
        {
          src:
            "https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit-icons.min.js",
        },
        {
          src: "https://cdnjs.cloudflare.com/ajax/libs/uikit/3.2.0/js/uikit.js",
        },
      ]}
      
    />
  )
}

SEO.defaultProps = {
  lang: `en`,
  meta: [],
  description: ``,
}

SEO.propTypes = {
  description: PropTypes.string,
  lang: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object),
  title: PropTypes.string.isRequired,
}

export default SEO

接下来创建我们自定义的css样式,创建文件 src/assets/css/main.css

a {  
  text-decoration: none !important;
}

h1 {  
  font-family: Staatliches !important;
  font-size: 120px !important;
}

#category {
  font-family: Staatliches !important;
  font-weight: 500 !important;
}

#title {
  letter-spacing: 0.4px !important;
  font-size: 22px !important;
  font-size: 1.375rem !important;
  line-height: 1.13636 !important;
}

#banner {
  margin: 20px !important;
  height: 800px !important;
}

#editor {
  font-size: 16px !important;
  font-size: 1rem !important;
  line-height: 1.75 !important;
}

.uk-navbar-container {
  background: #fff !important;
  font-family: Staatliches !important;
}

img:hover {  
  opacity: 1 !important;
  transition: opacity 0.25s cubic-bezier(0.39, 0.575, 0.565, 1) !important;
}

配置gatsby数据接口

为gatsby安装gatsby-source-strapi插件

yarn add gatsby-source-strapi

安装完成后修改gatsby根目录(frontend)下创建文件.env.development并输入以下内容

API_URL="http://localhost:1337"

这时候你可能注意到了,这个文件用于配置strapi的数据接口的,注意端口号后面不要带斜杠/

接下来我们修改统目录下的gatsby-config.js文件内容

require("dotenv").config({  
  path: `.env.${process.env.NODE_ENV}`,
})

module.exports = {  
  siteMetadata: {
    title: "My super blog",
    description: "Gatsby blog with Strapi",
    author: "Strapi team",
  },
  plugins: [
    "gatsby-plugin-react-helmet",
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: `${__dirname}/src/images`,
      },
    },
    {
      resolve: "gatsby-source-strapi",
      options: {
        apiURL: process.env.API_URL || "http://localhost:1337",
        contentTypes: [
          // List of the Content Types you want to be able to request from Gatsby.
          "article",
          "category",
        ],
        queryLimit: 1000,
      },
    },
    "gatsby-transformer-sharp",
    "gatsby-plugin-sharp",
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: "gatsby-starter-default",
        short_name: "starter",
        start_url: "/",
        background_color: "#663399",
        theme_color: "#663399",
        display: "minimal-ui",
      },
    },
    "gatsby-plugin-offline",
  ],
}

至此,前端数据接口已经准备完毕,进入下一章strapi的数据准备。

准备后端数据

为strapi设计数据结构并准备数据

登陆strapi后台,创建一个名为articlecontent type

准备创建content type

注意在新版strapi中创建content type的链接名称是Create new collection type,我们点击它然后在Display name里面输入article

创建content type

接下来准备为article创建字段

准备为article创建字段

需要创建的字段清单

  • title 类型:Text (required)
  • content 类型:Rich Text (required)
  • image 类型:Media (Single image) and (required)
  • published_at 类型:date (required)

完成后别忘记点击save保存,接下来需要配置访问权限

点击 Roles & Permission 并找到 public然后把findfindone前面的勾勾上然后点击Save保存。

配置数据访问权限

接下来我们添加一些数据

添加一些内容

在添加完内容后我们可以通过访问地址http://localhost:1337/articles看到刚刚我们添加的数据,只是数据格式为json。此外,我们还可以通过http://localhost:1337/graphql来访问graphql后台。

graphql后台管理页

分类的创建

有了blog内容了,但是我们还少了分类设置,接下来我们需要

  1. 创建一个名为categorycontent type并创建一个字段名为 name 类型为 Text,点击Save保存。
  2. article content type中创建一个名为category 类型为 Relation的新字段,注意说起来可能有点绕,article和category的关系是一对多(一个分类下有多个文章): Category has many Articles,做过关系型数据库设计的同学应该比较清楚,如果弄不明白就参考下图设置即可。
为文章创建分类字段

接下来依旧需要配置访问权限,点击 Roles & Permission 找到 public 然后找到category下面的 findfindone 并勾上,最后Save保存。

这时候去修改或创建一篇文章你会发现右侧有个分类的选项了,注意使用前需要事先添加好分类数据。

内容中已经出现分类选项

到了这一步我们已经把后端数据部分准备的差不多了,接下来我们需要去配置前端的数据展示。

前端数据的展示

为Gatsby设计数据展示的模板

在进入下一步操作前先删除掉创建gatsby默认项目带来的无用文件:

rm src/components/header.js src/components/layout.css src/components/image.js src/pages/page-2.js

需要了解的一些React组件基础知识

我们先更新两个文件pages/index.jscomponents/layout.js

pages/index.js 该文件为app的默认启动页(主页),在下面代码我们可以看到从components中导入了Layout组件和css文件

import React from "react"

import Layout from "../components/layout"

import "../assets/css/main.css"

const IndexPage = () => <Layout></Layout>

export default IndexPage  

components/layout.js

import React from "react"  
import PropTypes from "prop-types"

import Seo from "./seo"

const Layout = ({ children }) => {  
  return (
    <>
      <Seo />
      <main>{children}</main>
    </>
  )
}

Layout.propTypes = {  
  children: PropTypes.node.isRequired,
}

export default Layout  

Gatsby中的布局组件是什么?

这些组件用来作为你网站公共区域的部分,比如页面头部和尾部,还有常见的侧边栏和导航菜单等等,有了组件引入的概念后我们将这些区域做成组件在不同的页面直接引入使用即可。

创建导航条组件

这个组件的功能很明确,就是在页面头部显示所有分类,创建文件./src/components/nav.js并添加以下内容

import React from "react"  
import { Link, StaticQuery, graphql } from "gatsby"

const Nav = () => (  
  <div>
    <div>
      <nav className="uk-navbar-container" data-uk-navbar>
        <div className="uk-navbar-left">
          <ul className="uk-navbar-nav">
            <li>
              <Link to="/">Strapi Blog</Link>
            </li>
          </ul>
        </div>

        <div className="uk-navbar-right">
          <ul className="uk-navbar-nav">
            <StaticQuery
              query={graphql`
                query {
                  allStrapiCategory {
                    edges {
                      node {
                        strapiId
                        name
                      }
                    }
                  }
                }
              `}
              render={data =>
                data.allStrapiCategory.edges.map((category, i) => {
                  return (
                    <li key={category.node.strapiId}>
                      <Link to={`/category/${category.node.strapiId}`}>
                        {category.node.name}
                      </Link>
                    </li>
                  )
                })
              }
            />
          </ul>
        </div>
      </nav>
    </div>
  </div>
)

export default Nav  

这段代码是什么意思?

Gatsby v2 中添加了StaticQuery,一种全新的API可以让组件获取GraphQL query的数据。

query {  
  allStrapiCategory {
    edges {
      node {
        strapiId
        name
      }
    }
  }
}

这是一段最简单的GraphQL Query,它可以从strapi中查询所有分类数据,这时StaticQuery将数据记录生成html

render={data =>  
  data.allStrapiCategory.edges.map((category, i) => {
    return (
      <li key={category.node.strapiId}>
        <Link to={`/category/${category.node.strapiId}`}>
          {category.node.name}
        </Link>
      </li>
    )
  })
}

此时我们在Layout布局组件中导入刚刚创建的Nav导航组件,修改components/layout.js

import React from "react"  
import PropTypes from "prop-types"

import Nav from "./nav"  
import Seo from "./seo"

const Layout = ({ children, title}) => {  
  return (
    <>
      <Seo title={title} />
      <Nav />
      <main>{children}</main>
    </>
  )
}

Layout.propTypes = {  
  children: PropTypes.node.isRequired,
}

export default Layout  

这时候我们刷新页面可以看到分类信息被调用出来了

分类数据的调用

创建Articles组件

在首页要显示所有我们添加的文章我们需要创建一个组件来完成此功能,修改文件pages/index.js

import React from "react"  
import { StaticQuery, graphql } from "gatsby"

import Layout from "../components/layout"  
import ArticlesComponent from "../components/articles"

import "../assets/css/main.css"

const IndexPage = () => (  
  <Layout>
    <StaticQuery
      query={graphql`
        query {
          allStrapiArticle {
            edges {
              node {
                strapiId
                title
                category {
                  name
                }
                image {
                  publicURL
                }
              }
            }
          }
        }
      `}
      render={data => (
        <div className="uk-section">
          <div className="uk-container uk-container-large">
            <h1>Strapi blog</h1>
            <ArticlesComponent articles={data.allStrapiArticle.edges} />
          </div>
        </div>
      )}
    />
  </Layout>
)

export default IndexPage  

如你所见,这里我们使用了StaticQuery在ArticlesComponent组件中生成了数据。

接下来创建文件components/articles.js

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" data-uk-grid>
        <div>
          {leftArticles.map((article, i) => {
            return (
              <Card article={article} key={`article__${article.node.strapiId}`} />
            )
          })}
        </div>
        <div>
          <div className="[email protected] uk-grid-match" data-uk-grid>
            {rightArticles.map((article, i) => {
              return (
                <Card article={article} key={`article__${article.node.strapiId}`} />
              )
            })}
          </div>
        </div>
      </div>
    </div>
  )
}

export default Articles  

在这里,我们再一次用了Card这个组件,所以我们继续创建它components/card.js

import React from "react"  
import { Link } from "gatsby"

const Card = ({ article }) => {  
  return (
    <Link to={`/article/${article.node.strapiId}`} className="uk-link-reset">
      <div className="uk-card uk-card-muted">
        <div className="uk-card-media-top">
          <img
            src={article.node.image.publicURL}
            alt={article.node.image.publicURL}
            height="100"
          />
        </div>
        <div className="uk-card-body">
          <p id="category" className="uk-text-uppercase">
            {article.node.category.name}
          </p>
          <p id="title" className="uk-text-large">
            {article.node.title}
          </p>
        </div>
      </div>
    </Link>
  )
}

export default Card  

刷新页面

一个博客系统即将呈现

创建Article页面

到了这一步我们发现所有列表内容都能正常调用了,但是点击博客详细内容都不可用,接下来我们就要解决这个问题,首先修改gatsby根目录gatsby-node.js文件

exports.createPages = async ({ graphql, actions }) => {  
  const { createPage } = actions
  const result = await graphql(
    `
      {
        articles: allStrapiArticle {
          edges {
            node {
              strapiId
            }
          }
        }
      }
    `
  )

  if (result.errors) {
    throw result.errors
  }

  // Create blog articles pages.
  const articles = result.data.articles.edges
  articles.forEach((article, index) => {
    createPage({
      path: `/article/${article.node.strapiId}`,
      component: require.resolve("./src/templates/article.js"),
      context: {
        id: article.node.strapiId,
      },
    })
  })
}

你可以理解为该文件配置作用为生成了路由规则,作用参数为path: /article/${article.node.strapiId},系统会生成出如下格式的URL地址

  • /article/1
  • /article/2
  • /article/3
  • /article/4

在接下来的操作之前我们需要安装两个非常重要的插件:react-markdownreact-moment,显而易见一个是markdown语法解释器,而另一个用于时间格式化,执行安装

yarn add react-markdown react-moment moment

前面我们在gatsby-node.js中引入了article component:require.resolve("./src/templates/article.js",现在我们来创建这个文件

src/templates/article.js

import React from "react"  
import { graphql } from "gatsby"

import ReactMarkdown from "react-markdown"  
import Moment from "react-moment"

import Layout from "../components/layout"

export const query = graphql`  
  query ArticleQuery($id: String!) {
    strapiArticle(strapiId: { eq: $id }) {
      strapiId
      title
      content
      published_at
      image {
        publicURL
      }
    }
  }
`

const Article = ({ data }) => {  
  const article = data.strapiArticle
  return (
    <Layout title={article.title}>
      <div>
        <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={article.image.publicURL}
          data-srcset={article.image.publicURL}
          data-uk-img
        >
          <h1>{article.title}</h1>
        </div>

        <div className="uk-section">
          <div className="uk-container uk-container-small">
            <ReactMarkdown source={article.content} />
            <p>
              <Moment format="MMM Do YYYY">{article.published_at}</Moment>
            </p>
          </div>
        </div>
      </div>
    </Layout>
  )
}

export default Article  

记住,每次修改gatsby-node.js我们都要重启服务来查看效果。

文章已经可以正常浏览

创建Category页面

首先处理路由信息,修改gatsby-node.js文件

exports.createPages = async ({ graphql, actions }) => {  
  const { createPage } = actions
  const result = await graphql(
    `
      {
        articles: allStrapiArticle {
          edges {
            node {
              strapiId
            }
          }
        }
        categories: allStrapiCategory {
          edges {
            node {
              strapiId
            }
          }
        }
      }
    `
  )

  if (result.errors) {
    throw result.errors
  }

  // Create blog articles pages.
  const articles = result.data.articles.edges
  const categories = result.data.categories.edges

  articles.forEach((article, index) => {
    createPage({
      path: `/article/${article.node.strapiId}`,
      component: require.resolve("./src/templates/article.js"),
      context: {
        id: article.node.strapiId,
      },
    })
  })

  categories.forEach((category, index) => {
    createPage({
      path: `/category/${category.node.strapiId}`,
      component: require.resolve("./src/templates/category.js"),
      context: {
        id: category.node.strapiId,
      },
    })
  })
}

和上面处理article的方法一样,这里不做过多解释,接下来创建文件src/templates/category.js

import React from "react"  
import { graphql } from "gatsby"

import ArticlesComponent from "../components/articles"  
import Layout from "../components/layout"

export const query = graphql`  
  query Category($id: String!) {
    articles: allStrapiArticle(filter: { category: { id: { eq: $id } } }) {
      edges {
        node {
          strapiId
          title
          category {
            name
          }
          image {
            publicURL
          }
        }
      }
    }
    category: strapiCategory(strapiId: { eq: $id }) {
      name
    }
  }
`

const Category = ({ data }) => {  
  const articles = data.articles.edges
  const category = data.category.name

  return (
    <Layout title={category}>
      <div className="uk-section">
        <div className="uk-container uk-container-large">
          <h1>{category}</h1>
          <ArticlesComponent articles={articles} />
        </div>
      </div>
    </Layout>
  )
}

export default Category  

上面我们可以看出,再次使用了ArticlesComponent组件,事实上在index.js文件中我们也用了这个组件,组件复用好处显而易见是可以避免重复写相同的代码提供工作效率。

重启服务,点击分类就可以看到该类目下的内容了

分类页的实现

Read Comments

  • wood3 months ago0

    我不懂js,我不能理解的是,这种后台CMS在本地host的话,写博客什么的不是很麻烦吗?是不是适用于在VPS上部署?我知道的就是netlify CMS是可以直接在线使用的,还有其他简单的吗?

    • Julian3 months ago0

      任何CMS+GatsbyJS这种组合的搭配意义就是前后端分离,通常情况我们部署一个后端需要一个在线的运行环境,而有了Gatsby就可以把后端CMS部署在任何地方,不管是本地还是线上都可以,要说麻烦就是两点:1.Gatsby生成静态文件 2.往线上(gitpages,surge...)发布静态文件 一共需要运行两行命令

      可能你注意到了 后端可以部署在本地,那么有个比较有意思的事情就是不需要购入线上运行程序的VPS/Hosting了,后端数据在本地,安全又放心,前端文件在线上不用担心宕机情况发生,再部署个CDN,嗯 真香。

      如何选择?见仁见智,看个人喜好吧。

      • 2 months ago1

        大佬,大佬。本人想法跟大佬想的一样。。。 喜欢前后端分离。
        不过如果后端部署本地的话。有个问题就是没法线上实时更新文章了。
        还有就是打包啥的。。。 我觉得使用netlify的wekhook配合起来真香。
        但是线上MongoDB的数据库太贵了。。。免费版本才256mb。。。 嗨。。。

Post Comment