如何使用 Gatsby 和 Sanity.io 创建分类页面

Avatar of Knut Melvær
Knut Melvær

DigitalOcean 为您的旅程的每个阶段提供云产品。从 $200 免费信用 开始!

在本教程中,我们将介绍如何使用来自 Sanity.io 的结构化内容,使用 Gatsby 创建分类页面。您将学习如何使用 Gatsby 的节点创建 API 在 Gatsby 的 GraphQL API 中为您的内容类型添加字段。具体来说,我们将为 Sanity 的博客入门程序 创建类别页面。

话虽如此,这里没有什么是特定于 Sanity 的。无论您可能拥有哪个内容源,您都可以做到这一点。我们只是为了演示而使用 Sanity.io。

启动并运行博客

如果您想使用自己的 Gatsby 项目来学习本教程,请继续,跳到在 Gatsby 中创建新页面模板的部分。如果不是,请前往 sanity.io/create 并启动 Gatsby 博客入门程序。它会将 Sanity Studio 和 Gatsby 前端的代码放到您的 GitHub 帐户中,并在 Netlify 上为两者设置部署。所有配置,包括示例内容,都将到位,以便您可以直接深入了解如何创建分类页面。

项目启动后,请确保将 GitHub 上的新存储库克隆到本地,并安装依赖项。

git clone [email protected]:username/your-repository-name.git
cd your-repository-name
npm i

如果您想在本地同时运行 Sanity Studio(CMS)和 Gatsby 前端,您可以在项目的根目录中从终端运行命令 npm run dev。您也可以 cd 到 web 文件夹,并使用相同的命令运行 Gatsby。

您还应该安装 Sanity CLI 并从终端登录您的帐户:npm i -g @sanity/cli && sanity login。这将为您提供与 Sanity 项目交互的工具和有用命令。您可以添加 --help 标志以获取有关其功能和命令的更多信息。

我们将对 gatsby-node.js 文件进行一些自定义。要查看更改的结果,请重新启动 Gatsby 的开发服务器。这在大多数系统中都是通过在终端中按 CTRL + C 然后再次运行 npm run dev 来完成的。

熟悉内容模型

查看 /studio/schemas/documents 文件夹。我们有主要内容类型的架构文件:作者、类别、网站设置和帖子。每个文件都导出一个 JavaScript 对象,该对象定义了这些内容类型的字段和属性。在 post.js 中是类别字段定义。

{
  name: 'categories',
  type: 'array',
  title: 'Categories',
  of: [
    {
      type: 'reference',
      to: [{
        type: 'category'
      }]
    }
  ]
},

这将创建一个包含对类别文档的引用对象的数组字段。在博客的 Studio 中,它将看起来像这样。

An array field with references to category documents in the blog studio
博客 Studio 中包含对类别文档的引用的数组字段。

向类别类型添加 slug

转到 /studio/schemas/documents/category.js。类别有一个简单的内容模型,它包含标题和描述。现在我们要为类别创建专用页面,因此拥有 slug 字段也很方便。我们可以在模式中这样定义它。

// studio/schemas/documents/category.js
export default {
  name: 'category',
  type: 'document',
  title: 'Category',
  fields: [
    {
      name: 'title',
      type: 'string',
      title: 'Title'
    },
    {
      name: 'slug',
      type: 'slug',
      title: 'Slug',
      options: {
        // add a button to generate slug from the title field
        source: 'title'
      }
    },
    {
      name: 'description',
      type: 'text',
      title: 'Description'
    }
  ]
}

现在我们已经更改了内容模型,我们还需要更新 GraphQL 模式定义。通过在 Studio 文件夹中执行 npm run graphql-deploy(或者:sanity graphql deploy)来完成此操作。您会收到有关重大更改的警告,但由于我们只是添加了一个字段,因此您可以放心地继续。如果您希望该字段在 Netlify 上的 Studio 中可用,请将更改检入 git(使用 git add . && git commit -m"add slug field")并将其推送到您的 GitHub 存储库(git push origin master)。

现在我们应该浏览类别并为它们生成 slug。请记住点击发布按钮,使更改对 Gatsby 可用!如果您正在运行 Gatsby 的开发服务器,您还需要重新启动它。

关于 Sanity 源插件如何工作的简要说明

在开发中启动 Gatsby 或构建网站时,源插件将首先从已部署的 Sanity GraphQL API 获取 GraphQL 模式定义。源插件使用它来告诉 Gatsby 哪些字段应该可用,以防止它在某些字段的内容发生丢失时崩溃。然后,它将点击项目的导出端点,该端点将所有可访问的文档流式传输到 Gatsby 的内存中数据存储。

换句话说,整个网站都是通过两个请求构建的。运行开发服务器,还将设置一个监听器,它会将来自 Sanity 的任何更改实时推送到 Gatsby,而无需进行额外的 API 查询。如果我们给源插件一个具有读取草稿权限的令牌,我们将立即看到更改。这也可以通过 Gatsby 预览 体验到。

在 Gatsby 中添加类别页面模板

现在我们有了 GraphQL 模式定义和一些内容准备就绪,我们可以深入研究在 Gatsby 中创建类别页面模板。我们需要做两件事。

  • 告诉 Gatsby 为类别节点(这是 Gatsby 对“文档”的术语)创建页面。
  • 为 Gatsby 提供一个模板文件来生成包含页面数据的 HTML。

首先打开 /web/gatsby-node.js 文件。这里已经存在一些代码,可以用来创建博客帖子页面。我们将在很大程度上利用这段代码,但用于类别。让我们一步一步来。

createBlogPostPages 函数和以 exports.createPages 开头的行之间,我们可以添加以下代码。我在这里添加了一些注释来解释正在发生的事情。

// web/gatsby-node.js

// ...

async function createCategoryPages (graphql, actions) {
  // Get Gatsby‘s method for creating new pages
  const {createPage} = actions
  // Query Gatsby‘s GraphAPI for all the categories that come from Sanity
  // You can query this API on http://localhost:8000/___graphql
  const result = await graphql(`{
    allSanityCategory {
      nodes {
        slug {
          current
        }
        id
      }
    }
  }
  `)
  // If there are any errors in the query, cancel the build and tell us
  if (result.errors) throw result.errors

  // Let‘s gracefully handle if allSanityCatgogy is null
  const categoryNodes = (result.data.allSanityCategory || {}).nodes || []

  categoryNodes
    // Loop through the category nodes, but don't return anything
    .forEach((node) => {
      // Desctructure the id and slug fields for each category
      const {id, slug = {}} = node
      // If there isn't a slug, we want to do nothing
      if (!slug) return

      // Make the URL with the current slug
      const path = `/categories/${slug.current}`

      // Create the page using the URL path and the template file, and pass down the id
      // that we can use to query for the right category in the template file
      createPage({
        path,
        component: require.resolve('./src/templates/category.js'),
        context: {id}
      })
    })
}

最后,此函数需要位于文件的底部。

// /web/gatsby-node.js

// ...

exports.createPages = async ({graphql, actions}) => {
  await createBlogPostPages(graphql, actions)
  await createCategoryPages(graphql, actions) // <= add the function here
}

现在我们有了创建类别页面节点的机制,我们需要添加一个模板来描述它在浏览器中的实际外观。我们将基于现有的博客帖子模板来获得一致的样式,但在过程中保持简单。

// /web/src/templates/category.js
import React from 'react'
import {graphql} from 'gatsby'
import Container from '../components/container'
import GraphQLErrorList from '../components/graphql-error-list'
import SEO from '../components/seo'
import Layout from '../containers/layout'

export const query = graphql`
  query CategoryTemplateQuery($id: String!) {
    category: sanityCategory(id: {eq: $id}) {
      title
      description
    }
  }
`
const CategoryPostTemplate = props => {
  const {data = {}, errors} = props
  const {title, description} = data.category || {}

  return (
    <Layout>
      <Container>
        {errors && <GraphQLErrorList errors={errors} />}
        {!data.category && <p>No category data</p>}
        <SEO title={title} description={description} />
        <article>
          <h1>Category: {title}</h1>
          <p>{description}</p>
        </article>
      </Container>
    </Layout>
  )
}

export default CategoryPostTemplate

我们使用传递给 gatsby-node.js 中上下文的 ID 来查询类别内容。然后,我们使用它来查询类别类型上的 titledescription 字段。请确保保存这些更改后使用 npm run dev 重新启动,然后在浏览器中转到 localhost:8000/categories/structured-content。页面应该看起来像这样。

A barebones category page with a site title, Archive link, page title, dummy content and a copyright in the footer.
一个简单的类别页面。

很酷吧!但如果我们实际上可以看到属于此类别的帖子,那就更酷了,因为,好吧,这就是拥有类别的意义所在,对吧?理想情况下,我们应该能够在类别对象上查询“pages”字段。

在我们了解如何做到这一点之前,我们需要退一步来了解 Sanity 的引用是如何工作的。

查询 Sanity 的引用

即使我们只在一个类型中定义引用,Sanity 的数据存储也会“双向”索引它们。这意味着从帖子创建对“结构化内容”类别文档的引用会让 Sanity 知道该类别具有这些传入引用,并会在引用存在时阻止您删除它(引用可以设置为“弱”以覆盖此行为)。如果我们使用 GROQ,我们可以查询类别并 联接 具有它们的帖子,如下所示(在 groq.dev 上查看查询和结果)。

*[_type == "category"]{
  _id,
  _type,
  title,
  "posts": *[_type == "post" && references(^._id)]{
    title,
    slug
  }
}
// alternative: *[_type == "post" && ^._id in categories[]._ref]{

这将输出一个数据结构,使我们能够创建一个简单的类别帖子模板。

[
  {
    "_id": "39d2ca7f-4862-4ab2-b902-0bf10f1d4c34",
    "_type": "category",
    "title": "Structured content",
    "posts": [
      {
        "title": "Exploration powered by structured content",
        "slug": {
          "_type": "slug",
          "current": "exploration-powered-by-structured-content"
        }
      },
      {
        "title": "My brand new blog powered by Sanity.io",
        "slug": {
          "_type": "slug",
          "current": "my-brand-new-blog-powered-by-sanity-io"
        }
      }
    ]
  },
  // ... more entries
]

GROQ 没问题,GraphQL 呢?

关键是:截至目前,这种类型的查询无法在 Gatsby 的 GraphQL API 中开箱即用。但别担心!Gatsby 有一个强大的 API 用于更改其 GraphQL 架构,让我们可以添加字段。

使用 createResolvers 编辑 Gatsby 的 GraphQL API

Gatsby 在构建网站时将所有内容保存在内存中,并公开了一些 API,让我们可以深入了解它如何处理这些信息。其中包括 Node API。澄清一下,当我们在 Gatsby 中谈论“node”时,不要与 Node.js 混淆。Gatsby 的创建者从 图论 中借鉴了“边和节点”,其中“边”是“节点”之间的连接,而“节点”是实际内容所在的“点”。由于边是节点之间的连接,因此它可以具有“next”和“previous”属性。

The edges with next and previous, and the node with fields in GraphQL’s API explorer
带有 next 和 previous 属性的边,以及 GraphQL API 资源管理器中的带有字段的节点

Node API 主要由插件使用,但也可以用于自定义我们的 GraphQL API 的工作方式。其中一个 API 称为 createResolvers。它相当新,让我们可以深入了解类型节点是如何创建的,以便我们可以进行添加数据的查询。

让我们用它来添加以下逻辑

  • 在创建节点时检查具有 SanityCategory 类型的节点。
  • 如果节点匹配此类型,则创建一个名为 posts 的新字段,并将其设置为 SanityPost 类型。
  • 然后运行一个查询,过滤所有列出与当前类别 ID 匹配的类别的帖子。
  • 如果有匹配的 ID,将帖子节点的内容添加到此字段。

将以下代码添加到 /web/gatsby-node.js 文件中,可以放在现有代码的下方或上方

// /web/gatsby-node.js
// Notice the capitalized type names
exports.createResolvers = ({createResolvers}) => {
  const resolvers = {
    SanityCategory: {
      posts: {
        type: ['SanityPost'],
        resolve (source, args, context, info) {
          return context.nodeModel.runQuery({
            type: 'SanityPost',
            query: {
              filter: {
                categories: {
                  elemMatch: {
                    _id: {
                      eq: source._id
                    }
                  }
                }
              }
            }
          })
        }
      }
    }
  }
  createResolvers(resolvers)
}

现在,让我们重启 Gatsby 的开发服务器。我们应该可以在 sanityCategoryallSanityCategory 类型中找到一个新的帖子字段。

A GraphQL query for categories with the category title and the titles of the belonging posts

将帖子列表添加到类别模板

现在我们有了所需的数据,我们可以返回到我们的类别页面模板(/web/src/templates/category.js)并添加一个包含指向属于该类别的帖子的链接的列表。

// /web/src/templates/category.js
import React from 'react'
import {graphql, Link} from 'gatsby'
import Container from '../components/container'
import GraphQLErrorList from '../components/graphql-error-list'
import SEO from '../components/seo'
import Layout from '../containers/layout'
// Import a function to build the blog URL
import {getBlogUrl} from '../lib/helpers'

// Add “posts” to the GraphQL query
export const query = graphql`
  query CategoryTemplateQuery($id: String!) {
    category: sanityCategory(id: {eq: $id}) {
      title
      description
      posts {
        _id
        title
        publishedAt
        slug {
          current
        }
      }
    }
  }
`
const CategoryPostTemplate = props => {
  const {data = {}, errors} = props
  // Destructure the new posts property from props
  const {title, description, posts} = data.category || {}

  return (
    <Layout>
      <Container>
        {errors && <GraphQLErrorList errors={errors} />}
        {!data.category && <p>No category data</p>}
        <SEO title={title} description={description} />
        <article>
          <h1>Category: {title}</h1>
          <p>{description}</p>
          {/*
            If there are any posts, add the heading,
            with the list of links to the posts
          */}
          {posts && (
            <React.Fragment>
              <h2>Posts</h2>
              <ul>
                { posts.map(post => (
                  <li key={post._id}>
                    <Link to={getBlogUrl(post.publishedAt, post.slug)}>{post.title}</Link>
                  </li>))
                }
              </ul>
            </React.Fragment>)
          }
        </article>
      </Container>
    </Layout>
  )
}

export default CategoryPostTemplate

这段代码将生成这个简单的类别页面,其中包含一个链接帖子的列表——就像我们想要的那样!

The category page with the category title and description, as well as a list of its posts

开始制作分类页面吧!

我们刚刚完成了在 Gatsby 中使用自定义页面模板创建新页面类型的过程。我们介绍了 Gatsby 的一个 Node API,称为 createResolver,并使用它在类别节点中添加了一个新的 posts 字段。

这应该可以提供制作其他类型分类页面所需的一切!您的博客上有多个作者吗?好吧,您可以使用相同的逻辑来创建作者页面。使用 GraphQL 过滤器有趣的一点是,您可以使用它来超越通过引用建立的显式关系。它还可以用于使用正则表达式或字符串比较匹配其他字段。它相当灵活!