首页
/ Next.js Insight Error Page 编写与审计实战指南:从 FixCard 框架卡片到规范化文档结构

Next.js Insight Error Page 编写与审计实战指南:从 FixCard 框架卡片到规范化文档结构

2026-09-06 11:45:51作者:卓炯娓

本文基于 Next.js 仓库内置的 Agent 技能 SKILL.md,系统讲解 errors/<slug>.mdxkind: insight 类型错误页(Insight Error Page)的编写与审计方法。读完后你能掌握:如何用「来源链(Source of truth chain)」定位框架卡片数据与错误标题的源头、如何严格按规范模板产出与 dev overlay 修复卡片一一对应的文档页,以及如何用完整的审计清单逐项核对存量页面。

一、什么是 Insight Error Page

Next.js 的 Instant Navigations 功能会在开发期与构建期对「即时导航」做校验。当校验发现问题时,dev overlay 会展示一条错误标题(headline)和一组修复卡片(FixCard),而 errors/ 目录下的 MDX 页面就是这些错误的文档化落地:它们随仓库发布到 nextjs.org,与 dev overlay 中的卡片集保持镜像关系。

当前仓库中共有 16 个页面带有 kind: insight frontmatter,集中在 blocking-prerender-*instant-* 两类前缀下,例如 blocking-prerender-random.mdxinstant-shell-url-data.mdxblocking-route.mdx 等。

该技能覆盖两种工作模式:

  • 编写模式:「为 next-prerender-random 创建错误页面」「编写同步 IO 文档」;
  • 审计模式:「审计 blocking-prerender-dynamic 页面」「检查错误页面是否与框架一致」;
  • 以及一切涉及 errors/*.mdx insight 页面的任务。

一个关键的术语约定:frontmatter 中写 kind: insight,但正文一律称其为「errors」,绝不写「insights」。应写 "this error"、"error pages"、"dismiss the error"。

二、来源链:每个决定都要能追溯到源头

技能文件给出的核心方法是「来源链」——遇到任何不确定时去读源头,不要猜:

决定内容 来源文件 读取方式
卡片标题、ID、分组、代码片段、链接 URL instant-guidance-data.ts 每个 FixCard[] 数组就是一个错误族
错误标题(用户看到的字面文本) sync-io-messages.tsblocking-route-messages.ts createSyncIOErrorcreateDynamicBodyError 等工厂函数,模板字符串即标题
既有页面(需要保留的内容) 本仓库的 errors/<slug>.mdx 通读全文;无法放入修复卡片的内容迁移到 Gotchas 或 Other options
规范 API 文档(术语) 本仓库 docs/01-app/ 所有 API 名、指令名、概念都与发布版文档交叉核对
页面结构模板 技能文件本身 页面的规范形态
Vercel 写作风格 vercel/front 中的 vercel-technical-writing 技能(本仓库不含) 全量应用;技能中的「Voice and style」是最小化规则

框架卡片数据:FixCard[] 就是错误族

instant-guidance-data.ts 定义了 dev overlay 卡片的全部元数据。从源码可以看到卡片类型与分组体系(约 L31–L59):

export type FixCardGroup =
  | 'stream' | 'block' | 'cache' | 'static' | 'dynamic' | 'client'
  | 'defer' | 'measure' | 'ignore' | 'render' | 'upgrade' | 'disable'

export const FIX_CARD_GROUPS: Record<FixCardGroup, { label: string; color: CardColor; icon: FixCardIcon }> = {
  stream:  { label: 'Stream',  color: 'blue',   icon: 'align-left' },
  block:   { label: 'Block',   color: 'red',    icon: 'loading' },
  cache:   { label: 'Cache',   color: 'purple', icon: 'database' },
  // ... static / dynamic / client / defer / measure / ignore / render / upgrade / disable
}

export type FixCard = {
  id: string      // Docs anchor for this card.
  title: string
  group: FixCardGroup
  link: string | null  // Docs URL, or null for no link.
  snippets: Snippet[]
  copyable?: boolean   // Show the Copy prompt button on this card.
}

每个错误族对应一个具名数组,例如 runtimeCardsclientHookCardsdynamicCards。以 dynamicCards(对应 uncached data 错误族)为例,它包含三张卡:wrap-in-or-move-into-suspense(group: stream)、cache-the-component-or-data(group: cache)、allow-blocking-route(group: block),每张卡的 link 都指向 nextjs.org 上对应错误页面的锚点,snippets 是卡片里展示的高亮代码行。这正是文档页中 <FixCard /> 必须逐字复制的数据源。

错误标题:工厂函数的模板字符串就是页面 title

sync-io-messages.ts 中的 createSyncIOErrorImpl(L27–L43)展示了标题的生产方式:

return new Error(
  `Route "${route}": Next.js encountered the unstable value ${expression} while prerendering.\n\n` +
  `This value can change between renders, so it must be either prerendered or computed later.\n\n` +
  `Ways to fix this:\n` +
  `  - [dynamic] Render at request time by adding a dynamic data access (e.g. \`await connection()\`) before this call\n` +
  `  - [cache] Prerender and cache the value with \`"use cache"\`\n` +
  `  - [client] Render the value on the client with \`"use client"\`` + ...
)

去掉 Route "..." 前缀后的模板字符串,就是参考页 blocking-prerender-random.mdx frontmatter 中的 title: Next.js encountered the unstable value Math.random() while prerendering。同理,blocking-route-messages.tscreateRuntimeBodyErrorcreateDynamicBodyError 产出的 "encountered runtime data during prerendering" / "encountered uncached data during prerendering" 等模板,分别对应 blocking-prerender-runtimeblocking-prerender-dynamic 页面的标题,且正文中列出的修复项([stream][cache][block])与框架卡片分组一一对应。

三、动手前的五步准备

  1. 读框架卡片数据:在 instant-guidance-data.ts 中找到匹配的 FixCard[],记下每张卡的 idtitlegrouplinksnippets
  2. 读工厂消息:找到 createSyncIOErrorcreateSyncIOClientErrorcreateDynamicBodyError 等,标题模板(去掉 Route "..." 前缀)成为页面 title
  3. 读既有 errors/<slug>.mdx(如存在):记下每个模式、代码示例与注意事项。所有有用内容必须保留——新结构没有 1:1 位置时,迁移到 Gotchas 或 Other options。
  4. 读规范文档:对每个要引用的 API(use cachecacheLifecacheTagconnectionSuspenseuseEffectuse clientgenerateStaticParams 等)查阅发布版文档,使用其中的精确术语。
  5. 应用 Vercel 技术写作风格:主动语态、sentence-case 标题、无禁用词。

四、页面结构(强制形态)

每个页面遵循完全相同的形状,不得增删或重排小节:

---
title: <literal dev-overlay headline, no period, strip Route "..." prefix>
kind: insight
---

<Instant Navigations 提示框 div —— 链接博客文章与 Ensuring instant navigations 指南的样式化框;从任意现有 insight 页面复制>

<Framing,2-3 段。第 1 段以过去时态 "During <phase>, <event>" 开头(如 "During prerendering, a Server Component called ..."),点名 API 并说明机制或后果。绝不用内联代码开头——先用一个词起句("The `params` prop ...")。后续段落承载教学,并用 "For X, see Y" 句式交叉链接兄弟页面(平行 API 族 + 客户端/服务端对应页)。>

## Ways to fix this

<FixCardGrid> 包裹每张框架卡片一个 <FixCard />,按框架顺序排列

## <Card 1 title>
  Choose this fix when ...
  ### Patterns
  ### Trade-off
  ### Gotchas
  (可选: ### Short-lived caches —— 仅用于 cache 类修复)

## <Card 2 title>
  ...

## <Card N title>
  ...

(可选: ## Other options —— 不映射到框架卡片但仍然有用的模式。示例:桥接到另一错误页的修复(客户端页面上放 "Cache the value in a Server Component" 并链接服务端页)、来自 `errors/<slug>.mdx` 上游的旧内容、彻底绕开问题的替代 API。每个 option 有自己的 `###` 标题、说明文字、代码片段、指向完整讲解页的 "Learn more" 链接)

## Verifying the fix
  (规范两段 —— 见下文 "Verifying the fix" 规则)

## Don't want this validation?
  (规范退出块 —— 见下文规则。
   例外:同步 IO 页面用 "## Why `instant = false` doesn't clear this error" 替换,
   因为退出机制无法抑制 sync-IO abort)

## Related Insights
  (所有其他 insight 类错误页的完整列表,当前页省略)

参考页 blocking-prerender-random.mdx 完整展示了这一形态:开头提示框、Ways to fix this 下的 <FixCardGrid>(三张卡:Generate on every request / Cache the random value / Render on the client)、每张卡对应的修复小节(各含 Patterns/Trade-off/Gotchas,cache 卡额外含 Short-lived caches)、Verifying the fixWhy instant = false doesn't clear this error、以及按「body 错误 → metadata/viewport → 不稳定值错误(服务端后客户端)→ navigation 类」排序的 Related Insights 索引。

五、硬规则详解

5.1 Frontmatter

  • title = dev overlay 展示标题,无句末句号,即 overlay 显示的字符串(见 errors.tsx 中的标题字符串或 sync-io-messages.ts 等工厂),去掉 Route "..." 前缀,内联表达式做泛化(client-hook 的 overlay 里内联显示 `useSearchParams()`,而文档标题写的是 "in a Client Component")。
  • kind: insight 必须始终存在。

5.2 ## Verifying the fix

每个页面在退出块之前都有该小节;页面不存在顶层 Good to know,页面级提示放入相关修复小节的 Gotchas。固定两段:

  1. 可观测检查:「After applying a fix, reload the route and confirm the page immediately paints meaningful UI, with any <Suspense> fallbacks covering only the regions that stream in.」(navigation 类 insight 用 "navigate to the route and confirm the insight no longer appears in the dev overlay and ..." 替代 "reload the route")。随后是空壳警示句:包住整个页面主体的边界可以用一个空壳通过校验。
  2. 工具段落:dev overlay 指向失败组件;构建输出更简略。运行 next build --debug-prerender 获取完整用户帧堆栈,next build --debug-build-paths /dashboard /settings 针对特定路由迭代。措辞直接复制现有页面。

检查项要写成读者在浏览器中看到的,而不是框架产物("the static shell renders real content" 已因此被弃用——正确的修复后 fallback 是预期的,检查点在于它只覆盖流式区域)。

5.3 <FixCard> 卡片

  • 所有卡片包在单个 <FixCardGrid> 中(与 instant-navigation.mdx 使用同一组件)。
  • 每张框架卡片一个 <FixCard />,顺序与框架 FixCard[] 数组一致。
  • title = 框架卡片标题,逐字。若作为标题读起来别扭,先改框架,绝不改文档。
  • href = # + 标题的自动 slug(如 "Generate on every request" → #generate-on-every-request),必须与标题自动生成的一致。
  • group = 框架卡片分组(dynamiccacheclientstreamdefermeasureblockrenderignoreupgradedisablestatic),与 instant-guidance-data.tsFIX_CARD_GROUPS 的键一致。
  • snippets = 与 instant-guidance-data.ts 中匹配卡片的 snippets 数组相同,逐字复制。卡片上不放描述性文字——视觉由 snippets 承担。
  • 自闭合标签(<FixCard ... />),卡片无子节点。
  • 不放 prompt 属性。「Copy prompt」按钮在点击时由页面 URL 与卡片的 title + href 动态生成 prompt:Agent 收到指向规则文档并点名该修复的 prompt 后,会去读文档页(即用户当前所在的页面)获取全部约束与代码形态。这正是该技能存在的原因——文档页本身就是 prompt 的事实来源。

5.4 ## <Fix> 小节

  • 标题文本 = 卡片标题,逐字,自动 slug 化为上述 href
  • 首句:「Choose this fix when <condition>.」
  • ### Patterns:每个意义不同的修复形态一个 ####,每个含 1–2 句纯文字说明、一个简短可读的 jsx filename="app/..." 片段(完整、可复制、无 ...existing code...)、可选的 Learn more: 链接。
  • ### Trade-off:1 段,强制。要在本错误语境下描述权衡,而非泛泛的 API 权衡。若唯一的诚实权衡就是规范 API 行为(如「GSP 在列表变化时需要重建」),一句话带过并链到 API 参考。
  • ### Gotchas:要点列表,强制(至少 1 条)。
  • 可选 ### Short-lived caches 子节(cache 类修复记录 5 分钟阈值)。

5.5 代码片段

  • 必须是合法 React。不得在 Client Component 渲染期间内联展示不稳定 API(random、time、crypto)——那会造成 hydration mismatch。应延迟到 useEffect + useState 或事件处理器。
  • 惰性 useState 初始化器(如 useState(() => someUnstableCall()))在 SSR 期间执行——必须在 Gotchas 中警告。
  • useRef 惰性初始化模式对稳定 ID 有效(在 getter 函数中初始化,而非内联),仅适用于「计算一次并冻结」的值,不适用于「反映当前时刻」的值。
  • 代码块一律带 filename="app/..."
  • 当模式把渲染延迟到 hydration 之后(如 useEffect),Trade-off 必须链接 Preventing flash before hydration 指南。

5.6 交叉链接

  • Framing 段:链到兄弟页面(客户端 ↔ 服务端对应页、平行 API 族)。
  • Gotchas:警告 Client Component 内联渲染时链到 -client 页面。
  • Related Insights:所有其他 insight 类错误页的完整列表(当前页省略)。这是 Insight 族的索引而非精选短名单。顺序:body 错误 → metadata/viewport → 不稳定值错误(服务端后客户端)→ navigation 类。不要在该节加 API 参考或指南;API 与文件约定的链接应内联散布于正文。
  • 跨页模式链接:当一个页面上的修复在兄弟页有深入讲解时,本页只展示最常见模式并链到兄弟页。例如服务端页的 "Render on the client" 只展示一个客户端模式并链到 -client 页;客户端页的 "Other options" 桥接到服务端页的 cache 修复。不要在兄弟页间复制整个小节——每页保持精简,让兄弟页做规范参考。
  • 仅第一方来源:只链 nextjs.org/docs/*react.dev/*developer.mozilla.org/* 及其他规范第一方参考。绝不链个人博客、社区文章、会议演讲、X/Bluesky、GitHub gist 或任何第三方来源——包括作者自己的博客。若某第三方文章启发了某个模式,内化该想法并用自己的声音写出,不做引用。兄弟错误页、自家文档、一手 API 规范是唯一可接受目标。

5.7 术语(对照规范文档核验)

  • use cache 指令(正文中不写作 "use cache");Cache Components(大写);static shell(链 glossary#static-shell);cacheLife / cacheTag / revalidateTag / updateTag 使用发布名;connection() 来自 next/server;Client Component / Server Component(大写);prerendering 首次出现必须链接 glossary。

5.8 ## Allow blocking route 规范形态

当框架卡片集包含 instant = false(group 为 block,如各 *Cards 数组中的 allow-blocking-route 卡)时,使用规范的小节形态。所有含此修复的页面必须一致——偏离形态会让页面读起来像离群值。

引言:1 段说明 instant = false 做什么及权衡。可选第 2 段说明何时它「很少」是正确答案(例如客户端 hook 或 cache 修复中 Suspense 边界几乎总是可行的场景),并直接表述其罕见性。

模式:页面 body 类错误(runtime data、uncached data、client hooks)同时用 #### Opt the page out#### Opt the layout out;viewport 类错误只用 #### Opt the layout out(viewport 永远在 layout 上)。每个模式含 1–2 句适用时机说明、展示导出语句的 jsx filename="app/..." 片段、Learn more: 链接。模式片段之后放 "Use either pattern when:" 要点列表(2 条:layout shell 无意义 + 增量迁移;viewport 页只有 1 个模式时用单数表述),以及一句收尾:"Don't use this to dismiss the error. Choose Sibling fix A or Sibling fix B when either is feasible."

Trade-off:1 段,「Navigations to this route are not instant. The user waits for the full server render before any HTML arrives. Use this only when that latency is the deliberate cost of the route's purpose.」

Gotchas(强制,按此顺序):

  • instant = false 只对导出它的 segment 生效;后代 segment 仍按自身配置或全局默认值校验。
  • 该导出不禁用预渲染。路由仍会尽可能预渲染,只是退出即时导航校验。
  • 页面特定 gotcha(如 viewport 页追加框架合成路由的 gotcha)放在两条规范要点之后。

绝不在面向用户的正文中写「Confirm with the user that ...」类要点——页面是给用户读的,写给他们,而不是给 Agent。Agent 应应用的护栏属于 ### Patterns 下的代码形态指导(Agent 通过复制的 prompt 中的文档链接读到同一页面)。

5.9 ## Don't want this validation? 规范退出块

每个 insight 页面在 ## Related Insights 之前以规范退出块收尾——六个同步 IO 页面(random/current-time/crypto 及其 -client 变体)除外,它们用 ## Why instant = false doesn't clear this error 替换,因为 sync-IO abort 发生在预渲染路径上,退出机制无法抑制它(参见 blocking-prerender-random.mdx 末尾的同名小节)。退出块说明即时导航校验在 Cache Components 应用中默认运行,并给出按 segment、按子树、按应用三种退出方式。逐字复制:

## Don't want this validation?

Instant-navigation validation runs by default in Cache Components apps and is what surfaces this error.

- **One segment**: add `export const instant = false` to the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations.
- **Entire app**: set `experimental.instantInsights.validationLevel` to `'manual-warning'` in `next.config`. This limits validation to segments that explicitly export `instant`.

See Ensuring instant navigations for the full model.

5.10 写作风格

  • 每节以答案开头:「Choose this fix when ...」。
  • Sentence-case 标题,无句末句号;不用破折号表强调。
  • 禁用词:easyquicksimplejustverybasicallyobviouslyutilizefacilitateleveragerobustseamlesscutting-edgeinnovative
  • 无填充语:In this guide ...As mentioned above ...Let's take a look at ...It's worth noting ...
  • 主动语态 + 直接称呼读者:「You wrap the component」而非「the component is wrapped」。
  • 模式上不加 "Default." 标签(评审阶段已移除——模式没有默认)。
  • 散文不用分号——拆成两句,或用 ", and" 做省略式对比。
  • 绝不用内联代码开头——先用一个词起句("The params prop ...")。
  • 标题中的代码仅在指代真实 API 且大小写精确时允许(await connection()cacheLife);概念保持散文("Opt the page out")。
  • Learn more: 链接文本:指南用目标页精确标题("Streaming"、"Ensuring instant navigations");API 参考用与文档标题一致的不带括号的裸代码名([connection]、[io]、[searchParams]);第三方 API 保留规范拼写([performance.now()])。

六、审计清单

审计现有页面时逐项核对:

  • [ ] title = overlay 展示标题,无句号,内联表达式已泛化
  • [ ] frontmatter 含 kind: insight
  • [ ] 无顶层 Good to know;存在 ## Verifying the fix 且含两个规范段落(可观测检查 + --debug-prerender 工具)
  • [ ] 所有卡片包在单个 <FixCardGrid>
  • [ ] 每张框架卡片一个 <FixCard />,且按框架顺序
  • [ ] 每个 <FixCard />title 与卡片标题逐字一致
  • [ ] 每个 <FixCard />href = # + 标题自动 slug
  • [ ] 每个 <FixCard />group 与框架卡片分组一致
  • [ ] 每个 <FixCard />snippets 与框架卡片 snippets 数组逐字一致
  • [ ] 任何 <FixCard /> 上无 prompt 属性——复制按钮从 title + href + 页面 URL 生成 prompt
  • [ ] <FixCard /> 自闭合(无子节点、无描述文字)
  • [ ] 每个 ## <Fix> 标题与卡片标题逐字一致
  • [ ] 每个修复小节含 ### Patterns### Trade-off### Gotchas
  • [ ] 模式上无 "Default." 标签
  • [ ] 页面任何位置无 Confirm with the user ... 表述
  • [ ] 含 ## Allow blocking route 的页面符合规范形态(body 类错误用双模式;viewport 仅 Opt the layout out;"Use either pattern when" 列表;"Don't use this to dismiss the error" 收尾;规范 2 条 Gotchas)
  • [ ] 代码片段是合法 React(Client Component 渲染期间无内联 Math.random()
  • [ ] Gotchas 中对 useState(() => Math.random()) 有警告
  • [ ] 所有 API 参考在正文中内联链接
  • [ ] Framing 段交叉链接兄弟页面(正文内联链接承载大部分 API 引用)
  • [ ] 存在 ## Don't want this validation?,逐字符合规范块(同步 IO 页为 ## Why instant = false doesn't clear this error
  • [ ] 存在 ## Related Insights,列出所有其他 insight 类错误页(当前页省略)
  • [ ] 上游 errors/<slug>.mdx 内容已保留(必要时迁移至 Gotchas 或 Other options)
  • [ ] 术语与规范文档一致(已核验,非假设)
  • [ ] 已应用 Vercel 技术写作风格(无禁用词、主动语态、sentence-case 标题)
  • [ ] 框架卡片 link URL 指向正确的标题自动 slug(若不符,标记为框架侧待办)
  • [ ] cache 类修复下存在 Short-lived caches 子节(如适用)
  • [ ] 散文无分号;无段落以代码开头
  • [ ] Learn more: 文本符合标题/裸 API 约定,且每个目标都是该模式的最佳页面

七、文件位置与参考页

规范参考页是 errors/blocking-prerender-random.mdx。编写新页面时先读它,以匹配其精确结构、语气与细节深度:从 Math.random() 这个具体 API 出发,三个修复小节(每请求生成 / 缓存随机值 / 客户端渲染)各配完整可运行的 app/... 代码片段,cache 小节附 Short-lived caches 子节解释 5 分钟阈值,末尾以「Why instant = false doesn't clear this error` 说明同步 IO 错误的特殊性,Related Insights 按族排序索引全部 15 个兄弟页。这一页面是整套技能规则的活体样板。

八、小结:文档页是 overlay 与 Agent 之间的单一事实来源

这套技能的设计闭环在于:dev overlay 的 FixCard 数据(instant-guidance-data.ts)与错误标题(sync-io-messages.ts 等工厂)是「机器侧」事实;errors/<slug>.mdx insight 页面是「人读 + Agent 读」的文档侧事实。卡片上不带 prompt 属性而靠页面 URL + 卡片标题动态生成 prompt,意味着文档页本身成为 Agent 修复建议的唯一依据——这也解释了为何结构、术语、交叉链接、代码片段合法性都要作为硬规则约束。对贡献者而言,掌握「来源链 → 五步准备 → 强制页面结构 → 硬规则 → 审计清单」这条流水线,即可独立编写或审计 Next.js 仓库中任意一个 insight 类错误页面。

登录后查看全文
热门项目推荐
相关项目推荐