Gatsby分页实战:构建时静态分页原理与pageContext避坑指南
1. 项目概述:为什么在 Gatsby 里做分页不是“加个组件”那么简单
你刚用 Gatsby 搭好一个博客,写了二十篇技术笔记,首页一刷全堆出来——页面加载慢、首屏白屏时间长、用户划到底都找不到“下一页”按钮。这时候你搜“Gatsby 分页”,第一条就是gatsby-awesome-pagination,文档写着“一行代码搞定”,你兴冲冲 npm install,照着示例贴了三行代码,结果 build 报错:pageContext is not defined;再查,发现createPages里没传上下文;补上之后,点击第二页又 404;最后翻到 GitHub issues,满屏都是“pageContext丢失”“path不匹配”“gatsby-node.js配置后首页不渲染”的抱怨。这不是你代码写错了,而是 Gatsby 的分页根本就不是传统框架里“前端切页”的逻辑——它是在构建时(build time)把每一页都预生成成独立的 HTML 文件,而gatsby-awesome-pagination只是帮你自动化这个“批量创建静态页”的过程。它不处理数据来源,不决定分页粒度,不校验路径规则,更不会替你把当前页码塞进 GraphQL 查询变量里。换句话说,它是个精密但冷酷的“流水线调度器”,你得先搭好原料车间(数据层)、设定好模具规格(分页参数)、校准好传送带位置(路径与上下文),它才肯开工。我去年重构三个 Gatsby 站点的分页系统,踩过所有典型坑:从createPages中context对象漏传导致所有分页页 404,到pageContext在模板中被意外覆盖引发无限重定向,再到gatsby-awesome-pagination默认的path拼接规则和中文路径冲突导致 SEO 友好性归零。这篇文章不讲 API 列表,只讲你真正上线前必须亲手验证的七道关卡:数据怎么切、页码怎么传、路径怎么定、上下文怎么保、模板怎么接、SEO 怎么稳、错误怎么查。如果你正卡在createPages报错、分页链接跳转 404 或者第二页内容和第一页一模一样,那你不是缺教程,是缺一份按生产环境逐行调试过的操作手册。
2. 核心设计逻辑:理解 Gatsby 分页的本质与 gatsby-awesome-pagination 的真实角色
2.1 Gatsby 分页不是“前端交互”,而是“构建时静态页批量生成”
很多刚从 Next.js 或 VuePress 转来的开发者,第一反应是“加个分页组件,点击触发setState切数据”。这在 Gatsby 里完全行不通。Gatsby 的核心哲学是Build-time Static Generation—— 所有页面必须在gatsby build这一步就生成完毕,部署后服务器只返回纯 HTML,没有运行时数据请求。这意味着:
- 不存在“点击下一页 → 发起 GraphQL 请求 → 渲染新内容”这种流程;
- 所有分页页(如
/blog/page/2/、/blog/page/3/)都必须在构建阶段,由gatsby-node.js中的createPagesAPI 显式创建为独立页面节点; - 每个分页页的
pageContext必须包含该页所需的所有参数(如currentPage、limit、skip、totalPages),这些参数会注入到对应模板组件的props.pageContext中; - 模板组件(如
blog-list.js)在构建时就被编译,它接收pageContext后,通过useStaticQuery或graphql查询语句(注意:是编译期查询,非运行时)获取本页应显示的数据子集。
提示:你可以把 Gatsby 分页想象成印刷厂印书——
gatsby-awesome-pagination是自动装订机,但它不负责写文章(数据源)、不决定每章多少页(分页逻辑)、不设计封面(模板),它只按你给的“章节清单”(分页配置)和“纸张规格”(路径规则),把已写好的内容(GraphQL 查询结果)批量裁切成指定页数并装订成册(生成/page/2/index.html)。你漏掉任何一道前置工序,装订机就会卡死或装错。
2.2 gatsby-awesome-pagination 的真实定位:一个高阶 createPages 封装工具
gatsby-awesome-pagination的源码只有不到 200 行,它的核心价值不是提供炫酷 UI,而是解决createPages中重复性最高的三类问题:
- 分页计算自动化:根据总条目数(
totalCount)和每页条数(limit),自动算出totalPages、currentPage、skip值,避免手写Math.ceil(totalCount / limit)出错; - 路径生成标准化:统一处理
/page/:num/、/blog/:num/等常见路径格式,支持自定义pathPrefix和pathSuffix,防止手拼字符串导致路径不一致; - 上下文注入规范化:确保每个分页页的
context对象结构统一,包含currentPage、limit、skip、totalPages、previousPagePath、nextPagePath等关键字段,且类型安全(如currentPage强制为数字而非字符串)。
但它绝不做以下事:
- ❌ 不读取或操作你的 GraphQL 数据源(
allMarkdownRemark、allMdx等); - ❌ 不修改你的 GraphQL 查询语句(你仍需在模板中手动写
skip和limit参数); - ❌ 不处理
pageContext在模板中的使用逻辑(你仍需在blog-list.js中正确解构pageContext并传入查询); - ❌ 不兼容 Gatsby v5 的
createPagesEphemeral新 API(截至 2024 年中,它仍基于createPages)。
我实测过直接手写createPages分页逻辑:12 行代码搞定基础分页,但加上边界处理(首页不显示previousPagePath、末页不显示nextPagePath)、路径容错(/page/01/和/page/1/统一)、SEO 字段注入(canonicalURL),代码膨胀到 47 行且极易出错。gatsby-awesome-pagination把这部分稳定逻辑封装起来,让你专注在业务层——比如“如何让第一页显示 10 篇,后续页显示 8 篇”这种真实需求。
2.3 为什么 pageContext 是整个链条的“命门”?它的生命周期与常见陷阱
pageContext是 Gatsby 分页中唯一贯穿构建全流程的“数据信使”,它的完整生命周期如下:
| 阶段 | 操作者 | 关键动作 | 常见错误 |
|---|---|---|---|
| 1. 构建准备 | 你在gatsby-node.js中 | 调用createPage({ path, component, context: { currentPage: 1, limit: 10, skip: 0 } }) | 漏传context对象,或context里缺少currentPage字段 → 后续模板无法获取页码 |
| 2. 页面创建 | Gatsby 内核 | 将context序列化并注入到该页面的page-data.json中 | context包含函数或未序列化对象(如Date实例)→ build 失败 |
| 3. 模板渲染 | 你的blog-list.js组件 | 通过props.pageContext.currentPage读取值,并用于 GraphQL 查询变量 | 在useStaticQuery中误用pageContext(useStaticQuery无参数,不能动态传变量)→ 查询结果始终是第一页 |
| 4. 客户端导航 | Gatsby Link 组件 | 点击/page/2/时,从page-data.json中反序列化pageContext并传入组件 | path配置错误导致page-data.json404 → 页面白屏,控制台报Cannot read property 'currentPage' of undefined |
最致命的陷阱是pageContext类型错乱。gatsby-awesome-pagination默认生成的currentPage是数字类型,但如果你在createPages中手动拼接路径如path:/blog/page/${i}/``(i是循环索引),而i是字符串"1",那么pageContext.currentPage就是字符串"1"。当你的 GraphQL 查询写成skip: ${pageContext.currentPage * pageContext.limit - pageContext.limit}时,字符串"1" * 10结果是10(隐式转换),但"1" - 10却是NaN!我曾因此调试了 3 小时,最终发现是pageContext.currentPage被意外转成了字符串。解决方案永远只有一条:在createPages中显式转换parseInt(i, 10),并在模板中用typeof pageContext.currentPage === 'number'做断言。
3. 实操全流程:从零搭建可上线的 Gatsby 分页系统(含完整代码与避坑注释)
3.1 前置准备:确认数据源结构与分页策略
在动代码前,先明确两个硬性前提:
第一,你的数据源必须支持分页查询。
Gatsby 的 GraphQL 层要求数据节点(Node)具备id字段且全局唯一。以最常见的 Markdown 博客为例,确保gatsby-transformer-remark插件已启用,且你的 Markdown 文件有正确的 frontmatter:
--- title: "Gatsby 分页实战详解" date: "2024-05-20" slug: "/blog/gatsby-pagination" --- 正文内容...运行gatsby develop,打开http://localhost:8000/__graphql,执行以下查询验证数据可用性:
query { allMarkdownRemark( sort: { fields: [frontmatter___date], order: DESC } limit: 10 skip: 0 ) { totalCount edges { node { id frontmatter { title date } fields { slug } } } } }✅ 成功返回totalCount > 0且edges有数据,说明数据源就绪。
❌ 若totalCount为 0,请检查gatsby-source-filesystem的path是否指向正确的 Markdown 目录,或gatsby-transformer-remark是否启用。
第二,确定分页策略——这是影响用户体验的核心决策。
不要默认用 “每页 10 篇”。根据你的内容类型选择:
| 场景 | 推荐每页条数 | 理由 | 我的实际案例 |
|---|---|---|---|
| 技术博客(长文为主) | 6–8 篇 | 首屏加载快,避免用户滑动过久;单篇文章平均阅读时长 5+ 分钟,用户更倾向深度阅读单篇 | 我的 React 教程站,设为 7 篇,LCP(最大内容绘制)从 3.2s 降至 1.8s |
| 新闻聚合站(短摘要) | 12–15 篇 | 用户快速扫读,需要更高信息密度;摘要卡片高度固定,布局更可控 | 客户的行业资讯站,14 篇,跳出率下降 22% |
| 作品集展示(大图为主) | 9 篇 | 平衡图片加载与页面长度;9 是 3×3 网格的天然倍数,CSS Grid 布局无冗余 | 设计师个人站,9 篇,移动端滚动流畅度提升明显 |
注意:
limit值一旦设定,必须同步更新createPages和模板中的 GraphQL 查询。我见过太多人改了createPages的limit,却忘了改模板里的limit,导致第一页显示 10 篇,第二页只显示 5 篇(因为skip计算错位)。
3.2 安装与基础配置:四步完成 gatsby-awesome-pagination 集成
Step 1:安装依赖
npm install gatsby-awesome-pagination # 或 yarn add gatsby-awesome-pagination提示:无需额外安装
gatsby-plugin-page-creator或其他分页插件,gatsby-awesome-pagination是纯工具库,无运行时依赖。
Step 2:在 gatsby-node.js 中编写 createPages 逻辑(核心!)
// gatsby-node.js const { createPagination } = require("gatsby-awesome-pagination"); exports.createPages = async ({ graphql, actions }) => { const { createPage } = actions; // 1. 查询所有 Markdown 文章总数(关键!必须用 totalCount) const result = await graphql(` query { allMarkdownRemark( sort: { fields: [frontmatter___date], order: DESC } ) { totalCount } } `); if (result.errors) { throw result.errors; } const totalCount = result.data.allMarkdownRemark.totalCount; const postsPerPage = 8; // 与模板中保持一致! // 2. 使用 createPagination 生成分页配置 // 注意:path 参数必须以 / 开头,且结尾带 /(Gatsby 路由规范) createPagination({ createPage, // Gatsby 提供的 API component: require.resolve("./src/templates/blog-list.js"), // 模板路径,必须存在! totalCount, // 总文章数,必填 itemsPerPage: postsPerPage, // 每页条数,必填 pathPrefix: "/blog", // 分页路径前缀,可选,默认为 "/" // 以下为高级配置,按需开启 // resolvePagePath: ({ pageNumber }) => `/blog/archive/${pageNumber}/`, // 自定义路径生成函数 // context: { siteTitle: "My Blog" }, // 额外注入的全局上下文 }); // 3. 【重要】单独创建首页(/blog/),避免与分页页混淆 // 因为 createPagination 默认生成 /blog/page/1/,而首页通常是 /blog/ createPage({ path: "/blog/", component: require.resolve("./src/templates/blog-list.js"), context: { currentPage: 1, limit: postsPerPage, skip: 0, totalPages: Math.ceil(totalCount / postsPerPage), // 注入首页特有字段,如 banner 图片 isHomepage: true, }, }); };关键细节解析:
totalCount必须来自 GraphQL 查询的totalCount字段,不能用edges.length计算(因为edges默认只返回前 20 条,totalCount才是真实总数);pathPrefix: "/blog"意味着分页页路径为/blog/page/2/,而非默认的/page/2/,这直接影响 SEO 和用户感知;- 单独创建
/blog/首页是最佳实践。createPagination默认从第 1 页开始生成/blog/page/1/,但用户习惯访问/blog/,且/blog/和/blog/page/1/是两个不同 URL,需分别处理; context中的isHomepage: true是为首页定制样式留的钩子,比如首页显示 Banner,分页页不显示。
Step 3:创建分页模板(blog-list.js)
// src/templates/blog-list.js import React from "react"; import { graphql } from "gatsby"; import Layout from "../components/layout"; import BlogPostCard from "../components/blog-post-card"; const BlogListTemplate = ({ data, pageContext }) => { const { currentPage, totalPages, isHomepage } = pageContext; const posts = data.allMarkdownRemark.edges; // 1. 安全解构 pageContext,防止构建时崩溃 if (!pageContext || typeof pageContext.currentPage !== "number") { console.error("Invalid pageContext in blog-list.js:", pageContext); return <Layout><div>分页上下文错误,请检查 gatsby-node.js 配置</div></Layout>; } // 2. 生成分页导航链接(关键!路径必须与 createPagination 一致) const generatePagePath = (pageNum) => { if (pageNum === 1 && !isHomepage) { return "/blog/page/1/"; } if (pageNum === 1 && isHomepage) { return "/blog/"; } return `/blog/page/${pageNum}/`; }; return ( <Layout> <h1>{isHomepage ? "最新文章" : `第 ${currentPage} 页`}</h1> {/* 文章列表 */} <div className="blog-grid"> {posts.map(({ node }) => ( <BlogPostCard key={node.id} post={node} /> ))} </div> {/* 分页导航 */} <nav className="pagination" aria-label="文章分页导航"> <ul> {/* 上一页 */} {currentPage > 1 && ( <li> <a href={generatePagePath(currentPage - 1)}> ← 上一页 </a> </li> )} {/* 页码列表(简化版,生产环境建议用 ellipsis) */} {Array.from({ length: totalPages }, (_, i) => i + 1).map((num) => ( <li key={num}> <a href={generatePagePath(num)} aria-current={num === currentPage ? "page" : undefined} > {num} </a> </li> ))} {/* 下一页 */} {currentPage < totalPages && ( <li> <a href={generatePagePath(currentPage + 1)}> 下一页 → </a> </li> )} </ul> </nav> </Layout> ); }; export default BlogListTemplate; // 3. GraphQL 查询:必须包含 skip 和 limit 变量! export const pageQuery = graphql` query BlogListQuery($skip: Int!, $limit: Int!) { allMarkdownRemark( sort: { fields: [frontmatter___date], order: DESC } limit: $limit skip: $skip ) { totalCount edges { node { id excerpt(pruneLength: 200) frontmatter { title date(formatString: "YYYY-MM-DD") description } fields { slug } } } } } `;关键细节解析:
pageQuery中的$skip和$limit变量必须声明为Int!(非空整数),否则 GraphQL 编译失败;generatePagePath函数严格遵循createPagination的路径规则:首页用/blog/,分页页用/blog/page/{num}/,确保链接绝对准确;aria-current="page"是无障碍访问(a11y)标准,屏幕阅读器会告知用户“当前在第 3 页”,Google 也视其为 SEO 信号;- 模板开头的
pageContext安全校验是线上必备,避免因构建时pageContext缺失导致整个页面白屏。
Step 4:配置 gatsby-config.js(可选但推荐)
// gatsby-config.js module.exports = { plugins: [ // 其他插件... { resolve: `gatsby-plugin-canonical-urls`, options: { siteUrl: `https://www.yoursite.com`, }, }, ], };gatsby-plugin-canonical-urls会自动为每个分页页添加<link rel="canonical">标签,例如/blog/page/2/的 canonical 指向自身,防止 Google 把/blog/page/1/和/blog/当作重复内容。这是 SEO 基础项,务必开启。
3.3 深度优化:让分页不止于功能,更兼顾性能与体验
3.3.1 首屏性能优化:延迟加载非首屏分页数据
Gatsby 默认会为所有分页页(包括/blog/page/100/)在构建时生成完整 HTML。但如果博客只有 50 篇文章,/blog/page/100/就是无效页。更糟的是,allMarkdownRemark查询会拉取全部数据,即使某页只显示 8 篇,GraphQL 仍需遍历所有节点计算totalCount。优化方案:
方案 A:用gatsby-plugin-limit-node限制查询范围(推荐)
安装gatsby-plugin-limit-node,在gatsby-config.js中配置:
{ resolve: `gatsby-plugin-limit-node`, options: { type: `MarkdownRemark`, limit: 200, // 只处理最近 200 篇,超出的忽略 }, },方案 B:在 createPages 中预过滤数据(更精准)
修改gatsby-node.js的 GraphQL 查询,只取需要分页的子集:
// 替换原来的 totalCount 查询 const result = await graphql(` query { allMarkdownRemark( sort: { fields: [frontmatter___date], order: DESC } # 只查询可能被分页用到的数据,比如最近 200 篇 limit: 200 ) { totalCount edges { node { id } } } } `); const totalCount = result.data.allMarkdownRemark.totalCount; // 后续 createPagination 逻辑不变实测效果:某博客从 1200 篇文章优化到只处理最近 200 篇,
gatsby build时间从 4m23s 降至 1m18s,构建内存占用减少 65%。
3.3.2 用户体验增强:平滑滚动与加载状态反馈
纯静态分页点击后是硬跳转,体验生硬。添加简单 JS 增强:
// src/components/pagination.js import { navigate } from "gatsby"; export const handlePageClick = (e, path) => { e.preventDefault(); // 添加 loading 状态(如按钮变灰) const link = e.currentTarget; const originalText = link.textContent; link.textContent = "加载中..."; link.disabled = true; // 导航后恢复 navigate(path).then(() => { link.textContent = originalText; link.disabled = false; }); };在blog-list.js的分页链接中调用:
<a href={generatePagePath(currentPage - 1)} onClick={(e) => handlePageClick(e, generatePagePath(currentPage - 1))} > ← 上一页 </a>注意:此增强仅作用于客户端导航(Gatsby Link),服务端直出 HTML 仍保持原样,符合渐进增强原则。
4. 常见问题排查与独家避坑指南:那些文档里不会写的血泪教训
4.1 问题速查表:高频报错与一键修复方案
| 报错现象 | 根本原因 | 修复方案 | 我的实测耗时 |
|---|---|---|---|
pageContext is not defined | createPages中未传context对象,或context为空对象{} | 检查createPagination调用处,确认component路径正确且文件存在;在createPage调用前加console.log("Creating page with context:", context) | 8 分钟(首次) |
Cannot read property 'currentPage' of undefined | 模板中pageContext解构错误,或pageContext未注入 | 在模板开头添加if (!pageContext) { return <div>Loading...</div>; };检查gatsby-node.js中createPage的context是否有currentPage字段 | 12 分钟(路径拼写错误) |
| 点击分页链接跳转 404 | path配置与createPagination的pathPrefix不一致,或.htaccess重写规则缺失 | 运行gatsby build后检查public/blog/page/2/index.html是否存在;若存在但 404,检查服务器是否配置了FallbackResource /index.html(Apache)或try_files $uri $uri/ /index.html(Nginx) | 25 分钟(客户服务器 Nginx 配置未更新) |
| 所有分页页内容相同(都是第一页) | GraphQL 查询中skip和limit未使用pageContext变量,或变量名拼写错误(如currentPage写成currentPageNum) | 在pageQuery中打印console.log("Skip:", $skip, "Limit:", $limit);检查模板中pageContext字段名是否与createPagination生成的一致 | 3 分钟($skip写成$skpi) |
TypeError: Cannot convert undefined or null to object | pageContext中某个字段(如previousPagePath)为null,但在模板中直接解构使用 | 在模板中用可选链pageContext?.previousPagePath,或添加默认值const { previousPagePath = "" } = pageContext | 5 分钟(未处理边界情况) |
4.2 独家避坑技巧:来自 3 个生产站点的实战经验
坑 1:中文路径与 gatsby-awesome-pagination 的兼容性问题
当你设置pathPrefix: "/博客"时,createPagination会生成/博客/page/2/,但某些 CDN 或服务器对 UTF-8 路径支持不佳,导致 404。解决方案:强制使用拼音路径。在gatsby-node.js中:
const { createPagination } = require("gatsby-awesome-pagination"); const pinyin = require("pinyin"); // npm install pinyin // 将中文前缀转为拼音 const blogPrefix = "/bo-ke"; // 手动映射,或用 pinyin("博客").join("-") createPagination({ createPage, component: require.resolve("./src/templates/blog-list.js"), totalCount, itemsPerPage: 8, pathPrefix: blogPrefix, // 使用拼音前缀 });坑 2:createPagination的resolvePagePath函数陷阱
文档示例中resolvePagePath: ({ pageNumber }) =>/blog/${pageNumber}/,但pageNumber是数字,而createPagination内部会调用String(pageNumber)转字符串。如果你写成resolvePagePath: ({ pageNumber }) =>/blog/${pageNumber.toString().padStart(2, "0")}/,期望生成/blog/01/,但createPagination会再次调用String(),导致/blog/001/。正确写法:在函数内直接返回完整路径字符串,不依赖外部转换:
resolvePagePath: ({ pageNumber }) => { const paddedNum = String(pageNumber).padStart(2, "0"); return `/blog/${paddedNum}/`; },坑 3:Gatsby v4 升级 v5 后的createPagesEphemeral兼容问题
Gatsby v5 引入createPagesEphemeral用于临时页面,但gatsby-awesome-pagination仍基于createPages。解决方案:在gatsby-node.js中保留createPages,并显式禁用createPagesEphemeral的干扰:
exports.createPages = async ({ graphql, actions }) => { // 你的 createPagination 逻辑... }; // 显式导出空的 createPagesEphemeral,防止 Gatsby v5 自动调用 exports.createPagesEphemeral = async () => {};4.3 性能监控:如何验证分页优化是否生效
不要只看gatsby build时间,用真实指标验证:
Lighthouse 测试:
- 在
/blog/和/blog/page/2/分别运行 Lighthouse,对比First Contentful Paint (FCP)和Largest Contentful Paint (LCP); - 优化后目标:LCP ≤ 2.5s(移动端),FCP ≤ 1.5s。
- 在
构建日志分析:
运行gatsby build --verbose,搜索Created page,确认生成的分页页数量与Math.ceil(totalCount / limit)一致;
搜索allMarkdownRemark,确认 GraphQL 查询耗时是否显著下降。网络面板验证:
打开 Chrome DevTools → Network,刷新/blog/page/2/,查看page-data.json文件大小。优化前可能达 800KB(含全部文章数据),优化后应 ≤ 120KB(仅本页 8 篇数据)。
我给客户做的最后一次审计:分页页page-data.json从 1.2MB 降至 98KB,LCP 从 4.7s 降至 1.9s,Google Search Console 中“移动设备可用性”警告清零。
5. 进阶扩展:超越基础分页的实用场景实现
5.1 多分类分页:为不同标签/分类生成独立分页流
你的博客有React、Gatsby、Design三个标签,希望/tag/react/下的文章也支持分页。gatsby-awesome-pagination本身不支持多维度分页,但可以组合使用:
Step 1:在gatsby-node.js中为每个标签创建分页
// 获取所有标签 const tagResult = await graphql(` query { allMarkdownRemark { group(field: frontmatter___tags) { fieldValue totalCount } } } `); tagResult.data.allMarkdownRemark.group.forEach((tagGroup) => { const tagName = tagGroup.fieldValue; const tagCount = tagGroup.totalCount; if (tagCount > 0) { createPagination({ createPage, component: require.resolve("./src/templates/tag-list.js"), totalCount: tagCount, itemsPerPage: 6, pathPrefix: `/tag/${tagName.toLowerCase()}`, // /tag/react/ context: { tagName, // 传递标签名给模板 }, }); } });Step 2:在tag-list.js模板中,用pageContext.tagName过滤数据
export const pageQuery = graphql` query TagListQuery($skip: Int!, $limit: Int!, $tagName: String!) { allMarkdownRemark( filter: { frontmatter: { tags: { in: [$tagName] } } } sort: { fields: [frontmatter___date], order: DESC } limit: $limit skip: $skip ) { totalCount edges { node { # ... 字段 } } } } `;关键:
$tagName变量必须在createPagination的context中注入,并在pageQuery中声明。这样每个标签都有独立的分页逻辑,互不干扰。
5.2 服务端渲染(SSR)兼容:为动态数据源添加分页支持
如果博客部分内容来自 CMS(如 Contentful),需在gatsby-node.js中处理createPages时的异步数据获取:
exports.createPages = async ({ graphql, actions, reporter }) => { const { createPage } = actions; // 1. 从 Contentful 获取文章列表(假设已配置 gatsby-source-contentful) const contentfulResult = await graphql(` query { allContentfulBlogPost(sort: { fields: publishDate, order: DESC }) { totalCount edges { node { contentful_id title publishDate } } } } `); if (contentfulResult.errors) { reporter.panicOnBuild("Error loading Contentful data", contentfulResult.errors); } const totalCount = contentfulResult.data.allContentfulBlogPost.totalCount; const postsPerPage = 10; // 2. 创建分页(逻辑同 Markdown) createPagination({ createPage, component: require.resolve("./src/templates/contentful-blog-list.js"), totalCount, itemsPerPage: postsPerPage, pathPrefix: "/cms-blog", }); };此时contentful-blog-list.js的 GraphQL 查询需改为allContentfulBlogPost,pageContext字段名保持一致即可。gatsby-awesome-pagination对数据源完全无感,只关心totalCount和itemsPerPage。
5.3 PWA 离线分页支持:让分页页在无网时也能访问
Gatsby 默认的gatsby-plugin-offline会缓存所有生成的 HTML 页面,包括/blog/page/2/。但需确保:
gatsby-plugin-offline在gatsby-config.js中启用;gatsby build后,public/blog/page/2/index.html确实存在;- Service Worker 正常注册(检查浏览器 Application → Service Workers)。
测试方法:gatsby build→npx serve -s public→ 打开/blog/page/2/→ 点击 Chrome DevTools → Application → Service Workers → Click "Update on reload" → 断网 → 刷新页面。如果页面正常显示,说明离线分页已生效。
我在线上环境实测:用户在地铁无网时访问/blog/page/3/,Service Worker 返回缓存的 HTML,文章列表完整显示,仅评论区(动态加载)显示“离线中”。这是静态站点的天然优势,无需额外开发。
6. 最后的实操心得:一个老手的三条硬核建议
我在 Gatsby 生态里做了五年主题开发,亲手交付过 17 个含复杂分页的商业站点,最后分享三条不写在文档里、但每次都能救命的建议:
第一条:永远先写createPages,再写模板,最后写查询。
新手常犯的顺序是:先写好blog-list.js,再想怎么传pageContext,结果发现createPages里漏了字段。正确顺序是
