在本教程中,我们将介绍如何使用来自 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 中,它将看起来像这样。

向类别类型添加 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 来查询类别内容。然后,我们使用它来查询类别类型上的 title
和 description
字段。请确保保存这些更改后使用 npm run dev
重新启动,然后在浏览器中转到 localhost:8000/categories/structured-content
。页面应该看起来像这样。

很酷吧!但如果我们实际上可以看到属于此类别的帖子,那就更酷了,因为,好吧,这就是拥有类别的意义所在,对吧?理想情况下,我们应该能够在类别对象上查询“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
使用 createResolvers
Gatsby 在构建网站时将所有内容保存在内存中,并公开了一些 API,让我们可以深入了解它如何处理这些信息。其中包括 Node API。澄清一下,当我们在 Gatsby 中谈论“node”时,不要与 Node.js 混淆。Gatsby 的创建者从 图论 中借鉴了“边和节点”,其中“边”是“节点”之间的连接,而“节点”是实际内容所在的“点”。由于边是节点之间的连接,因此它可以具有“next”和“previous”属性。

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 的开发服务器。我们应该可以在 sanityCategory
和 allSanityCategory
类型中找到一个新的帖子字段。

将帖子列表添加到类别模板
现在我们有了所需的数据,我们可以返回到我们的类别页面模板(/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
这段代码将生成这个简单的类别页面,其中包含一个链接帖子的列表——就像我们想要的那样!

开始制作分类页面吧!
我们刚刚完成了在 Gatsby 中使用自定义页面模板创建新页面类型的过程。我们介绍了 Gatsby 的一个 Node API,称为 createResolver
,并使用它在类别节点中添加了一个新的 posts
字段。
这应该可以提供制作其他类型分类页面所需的一切!您的博客上有多个作者吗?好吧,您可以使用相同的逻辑来创建作者页面。使用 GraphQL 过滤器有趣的一点是,您可以使用它来超越通过引用建立的显式关系。它还可以用于使用正则表达式或字符串比较匹配其他字段。它相当灵活!
对我来说真是太及时了。我遇到了类别和帖子的确切用例。您的代码恰好直接放入了我的 gatsby-node 文件中。
Andy,太棒了——很高兴它对您有用!
大家好,
感谢 Knut 为 Sanity 做出的所有努力,太棒了。
我只是想分享我在 sanity-land slack 上找到的解决最后一个练习的解决方案:作者页面。
感谢 Vince Parulan
Vince Parulan 4 个月前
“你遗漏了 elemMatch 中的另一个嵌套对象 author”
所以解析器将是
SanityAuthor: {
posts: {
type: [‘SanityPost’],
resolve (source, args, context, info) {
console.log(source._id)
return context.nodeModel.runQuery({
type: ‘SanityPost’,
query: {
filter: {
authors: {
elemMatch: {
author: {
_id: {
eq: source._id
}
}
}
}
}
}
})
}
}
}
我错过了 elemMatch 中的 author。
感谢 Vince