从Gatsby.js(Next.js)的主题制作中学习【React.js × GraphQL】在博客系统中获取发布文章信息的方法
前一篇文章:【GraphQL入门】学习笔记,实现在 Next.js 制作的博客中获取文章信息的API。
最近为了试运行GraphQL,我制作了一个简易应用程序并且能理解其运行方式。不过,我目前还不明白要如何实现在next.js中运行,需要怎样的文件结构和编码。
我需要学习如何将GraphQL添加到已经开始制作的Next.js应用中。我尝试过搜索了很多,但由于我对阅读JavaScript代码的能力不足,所以无法理解。因此,我打算通过解剖已经实现了Next.js基础上的GraphQL的完整应用程序来探索实现方法。这是一种深入学习的方法。
※ 本文中介绍了在对Gatsby主题的操作情况有所了解后中断写作的过程。现在决定先尝试使用Next.js的API。
体验制作Gatsby主题以学习React × GraphQL。
参考的主题是gatsby-advanced-starter,该主题也在GatsbyJS官方网站上公开。
按照快速静态网站生成器GatsbyJs的入门解释作为参考,思考代码的含义。
为了了解GatsbyJS默认设置,可以查阅GatsbyJS官方文档。
参考文献:
– 介绍GatsbyJs的高速静态网站生成器
– GatsbyJS官方文档
– gatsby-advanced-starter
首先,将Gatsby网站运行起来。
可以使用以下命令来生成 Gatsby 项目。rootPath 是指定要生成的目录,如果不存在则会新建。starter 是指定由 Gatsby 提供的主题的 URL(例如 GitHub 仓库)。
$ gatsby new [rootPath] [starter]
如果选择了这个主题,那么应该这样做。
$ gatsby new gatsby-advanced-starter https://github.com/Vagr9K/gatsby-advanced-starter
服务器启动。
cd gatxby-advanced-starter
npm run develop
如果连接到http://localhost:8000/,那么该网站会被显示出来。
启动命令可以在npm脚本中定义。
$ gatsby develop
"scipts": {
"develop": "gatsby develop"
}
显示 Gatsby CLI 的各种命令详细信息的命令。
$ npx gatsby -h
The file structure of a Gatsby project.
虽然不是全部都准确,但我尝试将需要了解整体概念的部分以树状结构表达出来。毕竟,在学习Gatsby编码时,我们应该先掌握整体概念。
- cntents/
- 01-01-2019.md
- 01-02-2019.md
- data/
- SiteConfig.js
- node_modules/
- インストールしたnode.jsモジュール
- public/
- 生成されたhtmlファイル群
- src/
- favicon.png
- components/
- Header/
- Header.jsx
- Header.css
- Footer/
- Footer.jsx
- Footer.css
- SEO/
- SEO.jsx
- SEO.css
- SocialLinks/
- SocialLinks.jsx
- SocialLinks.css
- PostTags/
- PostTags.jsx
- PostTags.css
- PostListing/
- PostListing.jsx
- PostListing.css
- templates/
- category.jsx
- listing.jsx
- listing.css
- post.jsx
- post.css
- tag.jsx
- pages/
- about.jsx
- contact.jsxなど
- static/
- 静的ファイル(imgやtxtなど)
- gatsby-config.js
- gatsby-node.js
- jsconfig.json
- package.json
- package-lock.json
gatsby-node.js文件在网站构建过程中只会被执行一次,用于导出要使用的API。它应放置在网站的根路径。
参考文件
gatsby-node.js的API文件(官方文档)
接下来尝试创建发布文章页面。
使用React组件创建固定页面
在src/pages中创建React组件。
import React from 'react'
const About = () => (
<div>
<h1>About Page</h1>
<div>This is a About Page</div>
</div>
)
export default About
当连接到http://localhost:8000/about时,About页面会打开。在这里,使用Next.js与完全相同。如果只是一个固定页面的作品集网站,只需这样就足够了,但如果有像博客一样的文章更新,情况就不一样了。
关于Gatsby API的createPages部分
在Gatsby中,有一个名为createPages的API可以从页面模板的React组件中以编程方式创建页面。参考Gatsby主题的文件结构也包括一个components/文件夹和一个独立的templates/文件夹,具体如下所述。
简而言之,如果只使用React组件来构建博客网站,那么每篇文章都需要创建一个React组件的jsx文件,这与使用原始的HTML编码网站没有什么区别。因此,通过使用以React组件为原型的createPages机制来自动生成文章页面非常有用。
将用于固定页面的React组件放置在components/目录下,而用于文章页面的React组件则放置在templates/目录下。虽然它们都是React组件的根源,但区分它们并且使其易于管理和使用,将它们分开放置可以起到这样的作用。
手动传递个别文章信息数组的示例(不推荐)
为了理解createPages的机制,通常在gatsby-node.js中只需编写API设置,然后定义一个数组,并手动将文章信息放入该数组中,以便渲染页面进行练习。
在示例代码中,首先将文章信息的数组放入posts常量中,并使用createPages方法来遍历数组中的文章信息,并加载文章页面的路径(永久链接)、使用的React组件位置以及传递给React组件的变量定义以供渲染使用。
const path = require("path");
const posts = [
{
path: "posts/1",
date: "2019/01/01",
title: "投稿1"
},
{
path: "posts/2",
date: "2019/01/02",
title: "投稿2"
}
];
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions;
return new Promise((resolve, reject) => {
posts.forEach( post =>
createPage({
// ページのパス名
path: post.path,
// テンプレートとなるコンポーネントの指定
component: path.resolve(`src/templates/post.jsx`),
// テンプレートとなるコンポーネントに渡す変数
context: {
path: post.path,
date: post.date,
title: post.title
}
})
);
resolve();
});
};
将在上文中准备好的变量,按照指定的方式传递给下面的React组件。该组件会简单地显示文章标题和发布日期,并以指定的永久链接生成可查看的文章。
import React from 'react'
const Post = ({ pageContext: { title, path, date } }) => (
<div>
<h1>{title}</h1>
<div>{date}</div>
</div>
)
export default Post
打开应用程序。
gatsby develop
当连接到http://localhost:8000/post1时,将显示名为“投稿1”的文章。确认了createPages正在运行。
我毅然决然地利用Gatsby的功能,尝试生成发布文章页面。
用于通过GraphQL自动获取md文件文章信息的插件配置。
这是我最想了解的部分。为了实现在Next.js上构建博客的目标,我认为最简单快速的方式是使用React和GraphQL的最小组合。然而,由于要从md文件中获取文章信息,所以在Gatsby中需要安装一个Markdown插件,但这样就必须将其作为一个Gatsby模块来运行,这让人感觉有点像黑盒子,不太理想。我想了解其中的具体内容。
$ npm install --save gatsby-source-filesystem gatsby-transformer-remark
在gatsby-config.js文件中,将Gatsby专用插件列在plugins中,进行加载和配置。
gatsby-source-filesystem是讓Gatsby能夠從文件系統中讀取文件的工具。從這裡的設置方式來看,它類似於在node.js中使用__dirname等功能來讀取靜態文件的app.use函數。在resolve中,需要填入導入的插件名稱,options.path則是要應用的目錄,options.name可能是用於調用時的id之類的東西?
gatsby-transformer-remark是用于解析Markdown的工具。虽然有一些选项可供使用,但示例代码中并未设置。我对这一点不太了解。总之,在官方文档中有关于使用GraphQL发出查询的说明,也许这就是我想要的答案。
参考文献
gatsby源文件系统
gatsby转换器注释
module.exports = {
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/contents`,
name: "contents",
}
},
`gatsby-transformer-remark`,
]
}
创建一个描述新闻信息的md文件。
然后,通过Markdown在指定的插件加载设置中,将文章内容撰写为md文件,并创建在src/contents/路径下。据说,通过gatsby-transformer-remark插件,可以通过GraphQL解析md文件的Front Matter并进行获取。
---
path: "posts/post1"
date: "2019年1月1日"
title: "投稿1"
---
# 投稿1
記事の内容。
---
path: "posts/post2"
date: "2019年1月2日"
title: "投稿2"
---
# 投稿2
記事の内容。
请编写一段代码,通过GraphQL获取md文件的文章信息。
在gatsby-node.js中进行插件设置,在src/contents/目录中有md文件存储文章信息。然后在gatsby-config.js中编写处理并通过GraphQL获取解析后的md文件文章信息,生成页面的处理。
const path = require('path')
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
return graphql(`
{
allMarkdownRemark(
sort: {
order: DESC,
fields: [frontmatter___date]
}
limit: 1000
){
edges {
node {
frontmatter {
path
date
title
}
}
}
}
}
`).then(result => {
if (result.errors) return Promise.reject(result.errors)
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.frontmatter.path,
component: path.resolve(`src/templates/blog-template.js`),
context: {
path: node.frontmatter.path,
date: node.frontmatter.date,
title: node.frontmatter.title,
}
})
})
})
}
在上述的代码中的返回 graphql 部分使用 GraphQL 发出了查询。
在这段描述中,它提到了要获取MarkdownRemark节点的部分,可以参考官方文档中gatsby-transformer-remark的章节。
我最初想知道Remark是什么东西,于是在谷歌上搜索,发现了官方网站上的标语:”一个搭载了插件的Markdown处理器”。看起来”MarkdownRemark”这个词组意味着它使用了Markdown库。
Markdown的巨大世界,参考文献Remark.js官方网站。
根据官方网站的解释,据说将用于描述md文件文章的元信息的frontmatter字段转换为GraphQL字段,并在allMarkdownRemark节点(即allMarkdownRemark.edges.node)中被检测到。投稿文章的md文件的frontmatter中包含了三个元信息,即路径、日期和标题,并将其转换为GraphQL字段。
-
- allMarkdownRemark.edges.node.frontmatter.path
-
- allMarkdownRemark.edges.node.frontmatter.date
- allMarkdownRemark.edges.node.frontmatter.title
总而言之,allMarkdownRemark(参数)的意思是按日期顺序对Markdown格式的文章进行排序,并最多取得1000条。而allMarkdownRemark(){获取数据}的部分指定了文章的元信息。GraphQL的数据类型和模式定义可能在Gatsby插件内部完成。
在这里重新启动应用程序。
$ gatsby develop
当连接到http://localhost:8000/post1时,会显示“投稿1”。
在React组件内定义GraphQL模式
当在gatsby-node.js中定义GraphQL模式时,除非使用createPages,否则将不会通过GraphQL查询来生成文章。这为什么是个问题呢?可以推测,因为按规范只能提供一个名为gatsby-node.js的文件,所以如果想要根据场景使用不同的GraphQL查询,就需要将所有查询定义都写在同一个文件中,这将降低可读性,并且难以理解哪个查询适用于哪种情况。换句话说,这种方法缺乏可维护性。
在GatsbyJS中,可以在React组件中定义GraphQL的查询和模式,因此可以将“数据获取处理”,“JSX”和“查询发出”写在作为每篇文章模板的React组件文件中。这种方法的优点是可以将与生成页面内容相关的处理都写在一个文件中,从而更容易进行维护。
使用GraphQL进行的三个重要操作可以作为复习的一部分
-
- スキーマの定義
データ型の定義 type [Example]
クエリの定義 type Query
データ取得処理の定義 resolvers
クエリ発行 graphql``
我认为,其中的Schema定义是由Gatsby插件gatsby-transformer-remark完成的(预计),所以只需要考虑剩下的两个。这样说来,要在Gatsby主题中使用GraphQL,只需用React组件编写“数据获取处理”,“查询发送”和“JSX”即可。
exports.createPages = ({ graphql, actions }) => {
// 全体的に省略
context: {
path: node.frontmatter.path
}
}
只需一个选择,将以下内容用中国母语进行转述:
Gatsby将查询结果作为data属性返回给数据获取处理函数的props,因此请记住这一规范。以免困惑于”data从哪里来的!”。
import React from 'react'
import { graphql } from 'gatsby'
const BlogTemplate = ({ data }) => {
const {
markdownRemark: {
html,
frontmatter: {
title,
date
}
}
} = data
return (
<div>
<h1>{`${title} Page`}</h1>
<div>date : {date}</div>
<div dangerouslySetInnerHTML={{ __html: html }} />
</div>
)
}
export default BlogTemplate
export const pageQuery = graphql`
query($path: String!) {
markdownRemark(frontmatter: { path: { eq: $path } }) {
html
frontmatter {
date
title
}
}
}
`
在此重新启动应用程序。
$ gatsby develop
当连接至 http://localhost:8000/post1 时,将展示”投稿1″页面。成功获取了博客基本功能中的文章信息。同样,如果在 src/pages/index.js 等位置编写 GraphQL 的模式,还可以实现文章列表等内容的获取。
GraphQL的语法速查表
我个人的观点是,我选择按照这种感觉来区分。我希望更好地理解并清晰地表达这一点。
typeDefsはスキーマ定義。
graphql``と記述されていたらクエリ発行。
関数だったらデータ取得処理。
虽然我差点忘了,但是我们现在要讨论一下Gatsby主题的分析。
因为我想起这篇文章的主要目的是要实现在Next.js制作的博客投稿文章中添加分类和标签功能,所以我决定去解读已经实现了这些功能的Gatsby主题代码。学习解读代码的知识一直是我到目前为止的学习内容。要解读的Gatsby主题就是之前提到的gatsby-advanced-starter,首先我会将焦点放在该项目文件的gatsby-node.js、src/components/PostListing.jsx和src/templates/listing.jsx等地方,来考察代码的含义和相关性。
参考文献
gatsby-advanced-starter
由于这部分代码含有整个主题中多余的处理,导致解读困难,因此我决定先放在一边,然后仔细调查。暂时先附上代码示例和解读的草稿。
示例代码
/* eslint "no-console": "off" */
const path = require("path");
const _ = require("lodash");
const moment = require("moment");
const siteConfig = require("./data/SiteConfig");
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
let slug;
if (node.internal.type === "MarkdownRemark") {
const fileNode = getNode(node.parent);
const parsedFilePath = path.parse(fileNode.relativePath);
if (
Object.prototype.hasOwnProperty.call(node, "frontmatter") &&
Object.prototype.hasOwnProperty.call(node.frontmatter, "title")
) {
slug = `/${_.kebabCase(node.frontmatter.title)}`;
} else if (parsedFilePath.name !== "index" && parsedFilePath.dir !== "") {
slug = `/${parsedFilePath.dir}/${parsedFilePath.name}/`;
} else if (parsedFilePath.dir === "") {
slug = `/${parsedFilePath.name}/`;
} else {
slug = `/${parsedFilePath.dir}/`;
}
if (Object.prototype.hasOwnProperty.call(node, "frontmatter")) {
if (Object.prototype.hasOwnProperty.call(node.frontmatter, "slug"))
slug = `/${_.kebabCase(node.frontmatter.slug)}`;
if (Object.prototype.hasOwnProperty.call(node.frontmatter, "date")) {
const date = moment(node.frontmatter.date, siteConfig.dateFromFormat);
if (!date.isValid)
console.warn(`WARNING: Invalid date.`, node.frontmatter);
createNodeField({ node, name: "date", value: date.toISOString() });
}
}
createNodeField({ node, name: "slug", value: slug });
}
};
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
const postPage = path.resolve("src/templates/post.jsx");
const tagPage = path.resolve("src/templates/tag.jsx");
const categoryPage = path.resolve("src/templates/category.jsx");
const listingPage = path.resolve("./src/templates/listing.jsx");
// Get a full list of markdown posts
const markdownQueryResult = await graphql(`
{
allMarkdownRemark {
edges {
node {
fields {
slug
}
frontmatter {
title
tags
category
date
}
}
}
}
}
`);
if (markdownQueryResult.errors) {
console.error(markdownQueryResult.errors);
throw markdownQueryResult.errors;
}
const tagSet = new Set();
const categorySet = new Set();
const postsEdges = markdownQueryResult.data.allMarkdownRemark.edges;
// Sort posts
postsEdges.sort((postA, postB) => {
const dateA = moment(
postA.node.frontmatter.date,
siteConfig.dateFromFormat
);
const dateB = moment(
postB.node.frontmatter.date,
siteConfig.dateFromFormat
);
if (dateA.isBefore(dateB)) return 1;
if (dateB.isBefore(dateA)) return -1;
return 0;
});
// Paging
const { postsPerPage } = siteConfig;
const pageCount = Math.ceil(postsEdges.length / postsPerPage);
[...Array(pageCount)].forEach((_val, pageNum) => {
createPage({
path: pageNum === 0 ? `/` : `/${pageNum + 1}/`,
component: listingPage,
context: {
limit: postsPerPage,
skip: pageNum * postsPerPage,
pageCount,
currentPageNum: pageNum + 1
}
});
});
// Post page creating
postsEdges.forEach((edge, index) => {
// Generate a list of tags
if (edge.node.frontmatter.tags) {
edge.node.frontmatter.tags.forEach(tag => {
tagSet.add(tag);
});
}
// Generate a list of categories
if (edge.node.frontmatter.category) {
categorySet.add(edge.node.frontmatter.category);
}
// Create post pages
const nextID = index + 1 < postsEdges.length ? index + 1 : 0;
const prevID = index - 1 >= 0 ? index - 1 : postsEdges.length - 1;
const nextEdge = postsEdges[nextID];
const prevEdge = postsEdges[prevID];
createPage({
path: edge.node.fields.slug,
component: postPage,
context: {
slug: edge.node.fields.slug,
nexttitle: nextEdge.node.frontmatter.title,
nextslug: nextEdge.node.fields.slug,
prevtitle: prevEdge.node.frontmatter.title,
prevslug: prevEdge.node.fields.slug
}
});
});
// Create tag pages
tagSet.forEach(tag => {
createPage({
path: `/tags/${_.kebabCase(tag)}/`,
component: tagPage,
context: { tag }
});
});
// Create category pages
categorySet.forEach(category => {
createPage({
path: `/categories/${_.kebabCase(category)}/`,
component: categoryPage,
context: { category }
});
});
};
import React from "react";
import { Link } from "gatsby";
class PostListing extends React.Component {
getPostList() {
const postList = [];
this.props.postEdges.forEach(postEdge => {
postList.push({
path: postEdge.node.fields.slug,
tags: postEdge.node.frontmatter.tags,
cover: postEdge.node.frontmatter.cover,
title: postEdge.node.frontmatter.title,
date: postEdge.node.fields.date,
excerpt: postEdge.node.excerpt,
timeToRead: postEdge.node.timeToRead
});
});
return postList;
}
render() {
const postList = this.getPostList();
return (
<div>
{/* Your post list here. */
postList.map(post => (
<Link to={post.path} key={post.title}>
<h1>{post.title}</h1>
</Link>
))}
</div>
);
}
}
export default PostListing;
import React from "react";
import Helmet from "react-helmet";
import { graphql, Link } from "gatsby";
import Layout from "../layout";
import PostListing from "../components/PostListing/PostListing";
import SEO from "../components/SEO/SEO";
import config from "../../data/SiteConfig";
import "./listing.css";
class Listing extends React.Component {
renderPaging() {
const { currentPageNum, pageCount } = this.props.pageContext;
const prevPage = currentPageNum - 1 === 1 ? "/" : `/${currentPageNum - 1}/`;
const nextPage = `/${currentPageNum + 1}/`;
const isFirstPage = currentPageNum === 1;
const isLastPage = currentPageNum === pageCount;
return (
<div className="paging-container">
{!isFirstPage && <Link to={prevPage}>Previous</Link>}
{[...Array(pageCount)].map((_val, index) => {
const pageNum = index + 1;
return (
<Link
key={`listing-page-${pageNum}`}
to={pageNum === 1 ? "/" : `/${pageNum}/`}
>
{pageNum}
</Link>
);
})}
{!isLastPage && <Link to={nextPage}>Next</Link>}
</div>
);
}
render() {
const postEdges = this.props.data.allMarkdownRemark.edges;
return (
<Layout>
<div className="listing-container">
<div className="posts-container">
<Helmet title={config.siteTitle} />
<SEO />
<PostListing postEdges={postEdges} />
</div>
{this.renderPaging()}
</div>
</Layout>
);
}
}
export default Listing;
/* eslint no-undef: "off" */
export const listingQuery = graphql`
query ListingQuery($skip: Int!, $limit: Int!) {
allMarkdownRemark(
sort: { fields: [fields___date], order: DESC }
limit: $limit
skip: $skip
) {
edges {
node {
fields {
slug
date
}
excerpt
timeToRead
frontmatter {
title
tags
cover
date
}
}
}
}
}
`;
gatsby-node.js (盖茨比节点.js)
我事先将示例代码按照处理的各个部分进行了分割。一般情况下,在开头部分会按照惯例导入模块。
const path = require("path");
const _ = require("lodash");
const moment = require("moment");
const siteConfig = require("./data/SiteConfig");
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
let slug;
if (node.internal.type === "MarkdownRemark") {
const fileNode = getNode(node.parent);
const parsedFilePath = path.parse(fileNode.relativePath);
if (
Object.prototype.hasOwnProperty.call(node, "frontmatter") &&
Object.prototype.hasOwnProperty.call(node.frontmatter, "title")
) {
slug = `/${_.kebabCase(node.frontmatter.title)}`;
} else if (parsedFilePath.name !== "index" && parsedFilePath.dir !== "") {
slug = `/${parsedFilePath.dir}/${parsedFilePath.name}/`;
} else if (parsedFilePath.dir === "") {
slug = `/${parsedFilePath.name}/`;
} else {
slug = `/${parsedFilePath.dir}/`;
}
if (Object.prototype.hasOwnProperty.call(node, "frontmatter")) {
if (Object.prototype.hasOwnProperty.call(node.frontmatter, "slug"))
slug = `/${_.kebabCase(node.frontmatter.slug)}`;
if (Object.prototype.hasOwnProperty.call(node.frontmatter, "date")) {
const date = moment(node.frontmatter.date, siteConfig.dateFromFormat);
if (!date.isValid)
console.warn(`WARNING: Invalid date.`, node.frontmatter);
createNodeField({ node, name: "date", value: date.toISOString() });
}
}
createNodeField({ node, name: "slug", value: slug });
}
};
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
const postPage = path.resolve("src/templates/post.jsx");
const tagPage = path.resolve("src/templates/tag.jsx");
const categoryPage = path.resolve("src/templates/category.jsx");
const listingPage = path.resolve("./src/templates/listing.jsx");
// Get a full list of markdown posts
const markdownQueryResult = await graphql(`
{
allMarkdownRemark {
edges {
node {
fields {
slug
}
frontmatter {
title
tags
category
date
}
}
}
}
}
`);
if (markdownQueryResult.errors) {
console.error(markdownQueryResult.errors);
throw markdownQueryResult.errors;
}
const tagSet = new Set();
const categorySet = new Set();
const postsEdges = markdownQueryResult.data.allMarkdownRemark.edges;
// Sort posts
postsEdges.sort((postA, postB) => {
const dateA = moment(
postA.node.frontmatter.date,
siteConfig.dateFromFormat
);
const dateB = moment(
postB.node.frontmatter.date,
siteConfig.dateFromFormat
);
if (dateA.isBefore(dateB)) return 1;
if (dateB.isBefore(dateA)) return -1;
return 0;
});
// Paging
const { postsPerPage } = siteConfig;
const pageCount = Math.ceil(postsEdges.length / postsPerPage);
[...Array(pageCount)].forEach((_val, pageNum) => {
createPage({
path: pageNum === 0 ? `/` : `/${pageNum + 1}/`,
component: listingPage,
context: {
limit: postsPerPage,
skip: pageNum * postsPerPage,
pageCount,
currentPageNum: pageNum + 1
}
});
});
// Post page creating
postsEdges.forEach((edge, index) => {
// Generate a list of tags
if (edge.node.frontmatter.tags) {
edge.node.frontmatter.tags.forEach(tag => {
tagSet.add(tag);
});
}
// Generate a list of categories
if (edge.node.frontmatter.category) {
categorySet.add(edge.node.frontmatter.category);
}
// Create post pages
const nextID = index + 1 < postsEdges.length ? index + 1 : 0;
const prevID = index - 1 >= 0 ? index - 1 : postsEdges.length - 1;
const nextEdge = postsEdges[nextID];
const prevEdge = postsEdges[prevID];
createPage({
path: edge.node.fields.slug,
component: postPage,
context: {
slug: edge.node.fields.slug,
nexttitle: nextEdge.node.frontmatter.title,
nextslug: nextEdge.node.fields.slug,
prevtitle: prevEdge.node.frontmatter.title,
prevslug: prevEdge.node.fields.slug
}
});
});
// Create tag pages
tagSet.forEach(tag => {
createPage({
path: `/tags/${_.kebabCase(tag)}/`,
component: tagPage,
context: { tag }
});
});
// Create category pages
categorySet.forEach(category => {
createPage({
path: `/categories/${_.kebabCase(category)}/`,
component: categoryPage,
context: { category }
});
});
};
一旦在这里解读暂停。
下一步,尝试在Next.js的API路由中使用GraphQL进行运行。