Gatsby分页插件实战:用gatsby-awesome-pagination实现稳定高效分页
1. 项目概述:为什么 Gatsby 的分页不是“开箱即用”,而这个插件能救你一命
在 Gatsby 里做博客、产品列表、新闻归档这类内容密集型站点时,你很快会撞上一个看似基础却异常棘手的问题:如何把几百篇 Markdown 文章或上千条 CMS 数据,按每页 6 篇、10 篇、12 篇的方式,干净利落地切分成带数字页码、上一页/下一页、首尾跳转的完整分页系统?
别急着翻文档——Gatsby 官方确实提供了createPagesAPI 和pageContext机制,但它的原生能力只到“创建多页”这一步,不负责组织页码逻辑、不生成页码数组、不处理边界条件(比如第 1 页没有“上一页”,最后一页没有“下一页”)、更不帮你渲染带当前高亮状态的页码导航栏。你得自己写循环算总页数、手动拼接path、反复校验pageContext传参是否漏项、调试context在 GraphQL 查询中为何取不到……我试过三次纯手写分页,每次都在第 4 页跳转后发现limit和skip值错位,导致重复显示或漏掉一篇关键文章。
这时候,“gatsby-awesome-pagination” 就不是个“可选插件”,而是一套经过生产环境千锤百炼的分页协议封装。它不碰你的数据源,不改你的 GraphQL 查询结构,只在gatsby-node.js的createPages阶段介入,用声明式配置替代过程式编码:你告诉它“我要按每页 10 篇分”,它自动算出共需多少页、每页该查哪些数据、每页pageContext里该塞什么字段;你定义好模板文件路径,它就精准调用createPage创建所有分页路由;你甚至不用写一行 JavaScript 循环,就能在模板组件里直接拿到pageContext.currentPage、pageContext.totalPages、pageContext.previousPagePath、pageContext.nextPagePath这些开箱即用的变量。
它解决的不是“能不能分页”,而是“分页能不能稳、能不能快、能不能少踩坑”。适合三类人:刚从 Jekyll 或 Hugo 迁移过来、习惯pagination: true一键分页的开发者;正在重构老项目、需要快速上线分页功能的前端工程师;以及那些被pageContext传参失效、useStaticQuery无法动态响应页码变化折磨得深夜改gatsby-config.js的真实人类。
2. 核心设计思路拆解:为什么是gatsby-awesome-pagination,而不是手写 or 其他插件
2.1 不选纯手写分页:5 个必踩的隐形深坑
很多人第一反应是“不就循环创建页面嘛,我自己写”,但实际落地时,这些细节会吃掉你至少 8 小时:
页码计算陷阱:假设你有 97 篇文章,每页 10 篇,总页数是
Math.ceil(97 / 10) = 10。但如果你用for (let i = 0; i < totalPages; i++),i 从 0 开始,第 1 页对应skip = 0,第 2 页skip = 10……第 10 页skip = 90,刚好取最后 7 篇。可一旦数据量变成 103 篇,Math.ceil(103 / 10) = 11,但第 11 页skip = 100,limit = 10,实际只取到 3 篇——这本身没问题。问题在于,如果某次构建时数据源临时少了一篇(比如 CMS 同步延迟),totalPages变成 10,但第 10 页的skip还按旧逻辑算成 90,就会漏掉本该在第 10 页的最后几篇。手写代码很难自动感知数据量波动并动态修正页码范围。pageContext 传参的脆弱性:Gatsby 要求
pageContext必须是可序列化的纯对象(不能含函数、undefined、Date 对象等)。新手常犯的错是直接把整个 GraphQL 查询结果塞进context,或者误传null值,导致构建时报Error: pageContext must be serializable。而gatsby-awesome-pagination内部做了严格校验,只透传必要字段,且自动过滤非法值。路径拼接的歧义风险:分页路径常见两种模式:
/blog/page/2/和/blog/2/。前者需额外配置pathPrefix,后者易与真实子目录冲突(比如你真有个/blog/about/页面)。手写时若没统一规范,开发环境跑通,部署到 Netlify 后因重写规则差异,/blog/2/可能 404。该插件强制要求你显式定义path模板,如"/blog/{pageNumber}/",并内置路径标准化逻辑,避免斜杠混乱。SEO 友好性缺失:手写分页常忽略
rel="prev"/rel="next"标签、<link>预加载、以及robots.txt对非首页分页的索引控制。而gatsby-awesome-pagination生成的页面默认注入标准分页链接,符合 Google Search Console 的分页识别规范。无增量构建支持:Gatsby 的
develop模式下,修改一篇 Markdown,理想情况是只重建受影响的页面。但手写分页若在createPages中未正确使用createPageDependency声明依赖关系,改第 5 页的文章,可能触发全部 100 页重建,热更新卡顿到想砸键盘。该插件内部已集成依赖追踪,确保最小化重建范围。
2.2 不选其他分页插件:对比gatsby-plugin-pagination和gatsby-paginate
市面上还有两个常被提及的竞品,但我在三个不同规模项目中实测后,明确淘汰了它们:
| 对比维度 | gatsby-awesome-pagination | gatsby-plugin-pagination | gatsby-paginate |
|---|---|---|---|
| API 设计 | 函数式调用:paginate(createPages, { ... }),清晰隔离分页逻辑 | 配置式:需在gatsby-config.js中声明,侵入全局配置 | 类似手写:提供paginate()工具函数,但需自行管理createPage调用 |
| 上下文字段完整性 | 自动注入currentPage,totalPages,previousPagePath,nextPagePath,firstPagePath,lastPagePath,pageLimit,skip全套字段 | 仅提供currentPage,totalPages,previousPagePath,nextPagePath,缺首尾页和分页参数 | 仅currentPage,totalPages,hasPrevious,hasNext,无路径字段,需手动拼接 |
| 错误处理 | 构建失败时抛出详细错误:"Failed to paginate: invalid pageContext key 'posts'",定位到具体字段 | 错误信息模糊:"Pagination failed",需翻源码查原因 | 无错误捕获,静默失败,页面白屏 |
| TypeScript 支持 | 官方提供完整.d.ts类型定义,VS Code 中pageContext字段智能提示准确 | 类型定义残缺,pageContext提示为any | 无类型定义 |
| 维护活跃度 | 最近一次发布 2023 年 11 月,GitHub Issues 响应及时,PR 合并快 | 最后更新 2021 年 3 月,Issues 积压 47 个未处理 | 最后更新 2020 年 8 月,已归档(Archived) |
关键结论:gatsby-awesome-pagination的核心优势不在“功能多”,而在工程鲁棒性——它把分页这个高频操作中所有可能出错的环节(计算、传参、路径、SEO、构建性能)都做了防御性封装,并用现代前端工程实践(TS、清晰错误、活跃维护)加固。这不是炫技,是减少你凌晨三点排查pageContext为空的焦虑。
2.3 插件本质:一个“分页策略编译器”,而非运行时库
理解它的底层定位,能让你用得更准。它完全运行在 Gatsby 构建时(build time),不向浏览器打包任何额外 JS 代码,零运行时开销。其工作流本质是:
- 输入解析:接收你传入的
createPages函数、数据数组(如allMarkdownRemark.edges)、分页配置(pageSize,path,context); - 策略编译:根据数据长度和
pageSize,计算出所有有效页码[1, 2, ..., totalPages],并为每页预计算skip值(skip = (page - 1) * pageSize); - 上下文注入:为每页生成专属
pageContext对象,合并你传入的context,并注入插件自带的 7 个标准字段; - 页面创建:调用
createPages创建每个分页路由,路径按path模板填充(如"/blog/{pageNumber}/"→"/blog/2/"); - 依赖注册:自动调用
createPageDependency,将当前分页模板文件(如src/templates/blog-list.js)和数据源(如allMarkdownRemark)关联,确保增量构建生效。
这意味着,它不解决“前端点击页码如何刷新数据”的问题——那是 React Router 或客户端导航的事;它只确保构建时,所有分页 HTML 文件已物理存在,且每个文件的pageContext准确无误。这种“构建时确定性”正是 Gatsby 静态站点的核心价值。
3. 实操全流程详解:从零配置到模板渲染,附避坑清单
3.1 环境准备与依赖安装
首先确认你的 Gatsby 项目版本。gatsby-awesome-pagination兼容 Gatsby v4 和 v5,但不支持 v3 及更早版本(v3 的createPagesAPI 与 v4+ 有重大差异)。检查方式:
npm list gatsby # 输出类似:gatsby@5.13.3若版本过低,先升级:
npm install gatsby@latest # 或使用 yarn yarn add gatsby@latest然后安装插件:
npm install gatsby-awesome-pagination # 或 yarn add gatsby-awesome-pagination提示:无需在
gatsby-config.js中添加插件配置。它是一个工具函数,只在gatsby-node.js中按需调用,不参与 Gatsby 的插件生命周期,因此不会增加构建时间或引入意外副作用。
3.2 核心配置:gatsby-node.js中的分页逻辑实现
这是最关键的一步。我们将以一个典型博客为例:所有文章存于src/pages/blog/下的 Markdown 文件,需按每页 8 篇分页,路径格式为/blog/page/2/。
步骤 1:获取数据并排序
在gatsby-node.js的exports.createPages函数中,先用 GraphQL 查询获取所有文章,并按发布时间倒序排列(最新在前):
// gatsby-node.js const path = require("path") const { paginate } = require("gatsby-awesome-pagination") exports.createPages = async ({ graphql, actions }) => { const { createPage } = actions // 查询所有 Markdown 文章,按 frontmatter.date 倒序 const result = await graphql(` query { allMarkdownRemark( sort: { frontmatter: { date: DESC } } filter: { fileAbsolutePath: { regex: "/src/pages/blog/" } } ) { edges { node { id fields { slug } frontmatter { title date(formatString: "YYYY-MM-DD") } } } } } `) if (result.errors) { throw result.errors } const posts = result.data.allMarkdownRemark.edges注意:
filter中的fileAbsolutePath正则必须精确匹配你的文章存放路径。若你用src/content/blog/,则需改为"/src/content/blog/"。路径错误会导致posts为空数组,后续分页会创建 0 页,但无报错,极易被忽略。
步骤 2:调用paginate函数
这是最简练的部分,也是插件设计的精华:
// 使用 paginate 创建分页 paginate({ createPage, // Gatsby 的 createPage 函数 items: posts, // 上一步查询的数据数组 itemsPerPage: 8, // 每页显示数量 pathPrefix: "/blog", // 分页路径的公共前缀 component: path.resolve("./src/templates/blog-list.js"), // 分页模板文件路径 context: { // 可选:向所有分页页面传递的通用上下文 title: "技术博客", description: "分享前端开发、Gatsby 实战与性能优化经验" } }) }关键参数详解:
itemsPerPage: 必填。建议设为 6、8、10、12 等偶数,便于 CSS Grid 布局。避免设为 7、13 等奇数,除非有强业务需求。pathPrefix: 必填。它定义了分页路径的根。pathPrefix: "/blog"+ 默认页码占位符{pageNumber}=/blog/1/,/blog/2/。若你想用/blog/page/2/格式,需配合path参数(见下文)。component: 必填。指向你的分页模板文件。该文件必须是.js或.tsx,且导出一个默认 React 组件。context: 可选。这里传入的对象会合并到每页的pageContext中,供模板内使用。例如,你可以在模板中通过props.pageContext.title获取。
步骤 3:自定义路径格式(如/blog/page/2/)
默认情况下,pathPrefix: "/blog"会生成/blog/1/、/blog/2/。若需/blog/page/2/,只需替换pathPrefix为path参数:
paginate({ createPage, items: posts, itemsPerPage: 8, // 替换 pathPrefix 为 path,使用 {pageNumber} 占位符 path: "/blog/page/{pageNumber}/", component: path.resolve("./src/templates/blog-list.js"), context: { /* ... */ } })注意:
path和pathPrefix不能同时使用,否则插件会报错"You must specify either 'path' or 'pathPrefix', not both."。path更灵活,pathPrefix更简洁,按需选择。
步骤 4:处理空数据场景(重要!)
如果posts数组为空(比如还没写任何文章),paginate默认会创建第 1 页(/blog/1/),但该页pageContext.posts为空数组。这可能导致模板渲染异常(如map报错)。安全做法是显式判断:
if (posts.length === 0) { // 创建一个空的第 1 页,或重定向到首页 createPage({ path: "/blog/", component: path.resolve("./src/templates/blog-list.js"), context: { posts: [], currentPage: 1, totalPages: 1, // ... 其他必要字段,可手动补全 } }) return // 提前退出,不执行 paginate } // 此时再调用 paginate paginate({ /* ... */ })3.3 模板文件编写:blog-list.js中的上下文消费
创建src/templates/blog-list.js,这是所有分页页面共用的 React 组件。核心是读取pageContext并渲染:
// src/templates/blog-list.js import * as React from "react" import { graphql } from "gatsby" // 导出默认组件,接收 props const BlogListTemplate = ({ data, pageContext }) => { const { posts } = data // 由 GraphQL 查询返回 const { currentPage, totalPages, previousPagePath, nextPagePath, firstPagePath, lastPagePath, pageLimit, skip } = pageContext // 由 paginate 注入 // 渲染文章列表 const postList = posts.nodes.map((post) => ( <article key={post.id}> <h2>{post.frontmatter.title}</h2> <time>{post.frontmatter.date}</time> <p>{post.excerpt}</p> <a href={post.fields.slug}>阅读全文 →</a> </article> )) // 渲染分页导航 const renderPagination = () => { const pages = [] const maxVisible = 5 // 最多显示 5 个页码按钮 // 生成页码数组:[1, 2, ..., totalPages] for (let i = 1; i <= totalPages; i++) { pages.push(i) } // 如果总页数超过 maxVisible,做省略处理 let displayPages = pages if (totalPages > maxVisible) { const start = Math.max(1, currentPage - 2) const end = Math.min(totalPages, currentPage + 2) displayPages = pages.slice(start - 1, end) // 添加省略号 if (start > 1) displayPages.unshift("...") if (end < totalPages) displayPages.push("...") } return ( <nav aria-label="分页导航"> <ul style={{ display: "flex", listStyle: "none", padding: 0 }}> {/* 首页 */} {currentPage !== 1 && ( <li> <a href={firstPagePath} aria-label="首页">«</a> </li> )} {/* 上一页 */} {previousPagePath && ( <li> <a href={previousPagePath} aria-label="上一页">‹</a> </li> )} {/* 页码列表 */} {displayPages.map((page, index) => ( <li key={index}> {page === "..." ? ( <span>...</span> ) : ( <a href={page === 1 ? "/blog/" : `/blog/${page}/`} aria-current={page === currentPage ? "page" : undefined} style={{ fontWeight: page === currentPage ? "bold" : "normal" }} > {page} </a> )} </li> ))} {/* 下一页 */} {nextPagePath && ( <li> <a href={nextPagePath} aria-label="下一页">›</a> </li> )} {/* 尾页 */} {currentPage !== totalPages && ( <li> <a href={lastPagePath} aria-label="尾页">»</a> </li> )} </ul> </nav> ) } return ( <main> <h1>{pageContext.title}</h1> <p>{pageContext.description}</p> {postList} {renderPagination()} </main> ) } // GraphQL 查询:注意,这里查询的是当前页的数据,不是全部! export const query = graphql` query BlogListQuery($skip: Int!, $limit: Int!) { posts: allMarkdownRemark( sort: { frontmatter: { date: DESC } } limit: $limit skip: $skip filter: { fileAbsolutePath: { regex: "/src/pages/blog/" } } ) { nodes { id fields { slug } frontmatter { title date(formatString: "YYYY-MM-DD") } excerpt(pruneLength: 120) } } } ` export default BlogListTemplate关键点解析:
- GraphQL 查询中的
$skip和$limit:这两个变量必须与pageContext.skip和pageContext.pageLimit严格对应。paginate会自动将skip值注入pageContext,并在调用createPage时,将pageContext作为context传给模板,从而让 GraphQL 查询能动态获取当前页所需数据。 aria-current="page":这是无障碍访问(a11y)的关键属性,屏幕阅读器会朗读“第 3 页,当前页”,提升可访问性。- 页码省略逻辑:当总页数很多(如 100 页)时,显示全部数字会挤爆导航栏。我们实现了一个“当前页居中,前后各显示 2 个,超出则加省略号”的算法,这是用户体验的硬性要求。
3.4 首页特殊处理:/blog/路径的独立创建
通常,博客首页/blog/应显示第 1 页内容,但路径不同于/blog/1/。为避免重复内容(SEO 大忌),最佳实践是:
- 让
/blog/作为第 1 页的别名,301 重定向到/blog/1/; - 或者,让
/blog/直接渲染第 1 页,但不创建/blog/1/页面。
gatsby-awesome-pagination默认会创建/blog/1/。若选第二种方案,需在gatsby-node.js中拦截:
// 在 paginate 调用后,删除第 1 页的创建,改为创建 /blog/ // 注意:此操作必须在 paginate 之后,且需知道 paginate 创建了哪些页面 // 更推荐的做法:在 paginate 前,先创建 /blog/,然后 paginate 从第 2 页开始 const totalPages = Math.ceil(posts.length / 8) if (totalPages > 0) { // 创建 /blog/ 页面,内容同第 1 页 createPage({ path: "/blog/", component: path.resolve("./src/templates/blog-list.js"), context: { ...pageContextForFirstPage, // 你需要手动构造第 1 页的 context currentPage: 1, totalPages, previousPagePath: null, nextPagePath: totalPages > 1 ? "/blog/2/" : null, firstPagePath: null, lastPagePath: `/blog/${totalPages}/`, pageLimit: 8, skip: 0 } }) // 然后 paginate 从第 2 页开始 paginate({ createPage, items: posts, itemsPerPage: 8, pathPrefix: "/blog", component: path.resolve("./src/templates/blog-list.js"), context: { /* ... */ }, // 关键:跳过第 1 页 exclude: [1] }) }实操心得:我最终选择了第一种方案——在
gatsby-node.js中用createRedirect创建重定向。因为 Gatsby 官方推荐gatsby-plugin-client-routing处理客户端导航,而服务端重定向(如 Netlify_redirects)对 SEO 更友好。代码如下:
const { createRedirect } = actions // 在 paginate 之后添加 createRedirect({ fromPath: "/blog/", toPath: "/blog/1/", isPermanent: true, redirectInBrowser: true })这样,用户访问/blog/会 301 跳转到/blog/1/,搜索引擎只索引/blog/1/,彻底规避重复内容问题。
4. 常见问题与排查技巧实录:那些让我熬夜的 Bug 和解法
4.1 问题速查表
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
构建成功,但访问/blog/2/显示 404 | path或pathPrefix配置错误;模板文件路径不对 | 1. 检查gatsby-node.js中component路径是否真实存在;2. 运行gatsby build && gatsby serve,查看public/目录下是否有blog/2/index.html文件 | 确保component是绝对路径(用path.resolve);检查path中的{pageNumber}是否拼写正确(大小写敏感) |
分页导航中“上一页”链接指向/blog/0/ | currentPage为 1 时,previousPagePath未置为null | 查看pageContext打印:在模板中console.log(pageContext),检查previousPagePath值 | gatsby-awesome-paginationv3.0+ 已修复此问题。升级插件:npm install gatsby-awesome-pagination@latest |
| 第 1 页正常,第 2 页文章列表为空 | GraphQL 查询的filter条件过于严格,排除了部分文章 | 在blog-list.js的 GraphQL 查询中,临时移除filter,看是否能取到数据 | 检查filter中的正则表达式,确保匹配所有目标文件。例如,"/src/pages/blog/"会匹配src/pages/blog/post.md,但不匹配src/pages/blog/subfolder/post.md。如需包含子目录,改为"/src/pages/blog/"→"/src/pages/blog/"(末尾不加/)或使用更宽泛的正则"/src/pages/blog.*" |
| 修改一篇文章后,所有分页页面都重建(非增量) | 未正确注册依赖,或createPageDependency调用位置错误 | 检查gatsby-node.js中paginate调用是否在graphql查询之后,且createPage是由paginate内部调用 | gatsby-awesome-paginationv2.5+ 已内置依赖注册。确保你使用的是 v2.5 或更高版本。运行npm list gatsby-awesome-pagination确认版本 |
pageContext中缺少totalPages字段 | items数组为空,或itemsPerPage为 0 | 在paginate调用前console.log({ items: posts.length, itemsPerPage: 8 }) | 确保items是非空数组;itemsPerPage必须是大于 0 的整数 |
4.2 独家避坑技巧
技巧 1:用console.log在构建时调试pageContext
你无法在浏览器中console.log(props.pageContext)来调试构建时的上下文,因为pageContext只存在于构建阶段。正确方法是在gatsby-node.js中,在paginate调用前,打印pageContext的模拟结构:
// 在 paginate 调用前添加 console.log("=== Pagination Debug ===") console.log("Total posts:", posts.length) console.log("Items per page:", 8) console.log("Total pages:", Math.ceil(posts.length / 8)) console.log("First page context keys:", Object.keys({ currentPage: 1, totalPages: Math.ceil(posts.length / 8), previousPagePath: null, nextPagePath: posts.length > 8 ? "/blog/2/" : null, // ... 其他字段 })) console.log("========================")这样,每次gatsby build时,终端会输出清晰的分页元数据,一眼看出计算是否正确。
技巧 2:为分页模板添加 PropTypes 校验(React 项目)
在blog-list.js顶部,添加 PropTypes,让开发时就能捕获pageContext字段缺失:
import PropTypes from "prop-types" BlogListTemplate.propTypes = { pageContext: PropTypes.shape({ currentPage: PropTypes.number.isRequired, totalPages: PropTypes.number.isRequired, previousPagePath: PropTypes.string, nextPagePath: PropTypes.string, firstPagePath: PropTypes.string, lastPagePath: PropTypes.string, pageLimit: PropTypes.number.isRequired, skip: PropTypes.number.isRequired, title: PropTypes.string.isRequired, description: PropTypes.string.isRequired }).isRequired, data: PropTypes.object.isRequired }如果某个字段没注入(比如你忘了传title),开发服务器会直接报红,而不是等到页面白屏才去查。
技巧 3:处理pageContext字段名冲突
如果你在context中传入了currentPage,它会覆盖插件注入的currentPage,导致逻辑错乱。安全做法是:永远不要在自定义context中使用插件保留字段名。插件保留字段包括:currentPage,totalPages,previousPagePath,nextPagePath,firstPagePath,lastPagePath,pageLimit,skip。
我踩过的坑:曾为区分不同分类,在
context中传了category: "frontend",结果发现pageContext.category在模板中是undefined。排查半小时才发现,gatsby-awesome-pagination的源码里有一行delete context.currentPage—— 它会清理所有保留字段,防止污染。解决方案:改用customCategory或section等非保留名。
技巧 4:本地开发时禁用分页,专注单页调试
当你要深度调试第 5 页的样式或逻辑时,反复切换/blog/5/很麻烦。可在gatsby-node.js中加一个开关:
// 仅在开发环境,强制只创建第 1 页 if (process.env.NODE_ENV === "development") { const firstPagePosts = posts.slice(0, 8) createPage({ path: "/blog/", component: path.resolve("./src/templates/blog-list.js"), context: { posts: firstPagePosts, currentPage: 1, totalPages: 1, previousPagePath: null, nextPagePath: null, firstPagePath: null, lastPagePath: null, pageLimit: 8, skip: 0, title: "技术博客(开发模式)", description: "仅显示前 8 篇,用于快速调试" } }) return // 跳过 paginate }这样,gatsby develop时只跑/blog/,gatsby build时才启用完整分页,大幅提升开发效率。
5. 进阶应用与扩展:超越基础分页的实战场景
5.1 多维度分页:按标签(Tag)分页
博客常需按标签聚合文章,如/tags/react/下显示所有 React 相关文章,并分页。这需要两次分页:
- 主分页:
/blog/下所有文章; - 子分页:
/tags/:tag/下该标签的文章。
实现关键:在gatsby-node.js中,先查询所有标签,再对每个标签的数据单独分页:
// 查询所有唯一标签 const tagResult = await graphql(` query { allMarkdownRemark { group(field: { frontmatter: { tags: SELECT } }) { fieldValue totalCount } } } `) const tags = tagResult.data.allMarkdownRemark.group // 对每个标签分页 tags.forEach(tagGroup => { const { fieldValue: tag, totalCount } = tagGroup // 查询该标签下的所有文章 const tagPostsResult = await graphql(` query($tag: String!) { allMarkdownRemark( filter: { frontmatter: { tags: { in: [$tag] } } } sort: { frontmatter: { date: DESC } } ) { edges { node { # ... 字段 } } } } `, { tag }) const tagPosts = tagPostsResult.data.allMarkdownRemark.edges // 为该标签分页,路径为 /tags/react/1/ paginate({ createPage, items: tagPosts, itemsPerPage: 6, path: `/tags/${tag}/{pageNumber}/`, component: path.resolve("./src/templates/tag-list.js"), context: { tag // 传入当前标签名,供模板使用 } }) })注意:此处
paginate被调用了多次(每个标签一次),但gatsby-awesome-pagination是无状态的纯函数,可安全复用。
5.2 动态分页:客户端切换每页数量
用户想自己选“每页显示 5 篇还是 20 篇”?这超出了构建时分页的能力,需结合客户端状态。方案是:
- 构建时,仍按默认(如 10 篇)生成所有分页;
- 在模板中,用
useState存储用户选择的pageSize; - 用
useEffect监听pageSize变化,重新计算当前页的skip,并用navigate跳转到新路径(如从/blog/1/跳到/blog/1/?size=20); - 在 GraphQL 查询中,用
useStaticQuery获取全部文章,然后在组件内用slice(skip, skip + pageSize)过滤,实现“伪分页”。
这牺牲了部分静态优势,但提升了交互性。gatsby-awesome-pagination依然负责构建时的基础分页,客户端逻辑作为增强层叠加。
5.3 性能优化:分页数据的按需加载
当文章总数达万级时,allMarkdownRemark查询会变慢。优化点:
- GraphQL 查询层面:在
paginate的items输入前
