Writing Skills
writing-skills
效率与协作技能,来自 obra/superpowers,查看原文获取完整用途和操作说明。
nextjs-ops【部署与运维】Next.js App Router operations - the server/client boundary, the two caching models, Server Actions security, streaming, proxy.ts and deployment. Use for: next.js, nextjs, app router, use cache, cacheComponents, Cache Components, cacheLife, cacheTag, revalidateTag, updateTag, revalidatePath, unstable_cache, PPR, partial prerendering, stale data in pro…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
The App Router as an operational surface: what crosses the server/client
boundary, which caching model the app is actually on, why a response is stale,
and what a Server Action really is on the wire. React itself belongs to
react-ops; styling to tailwind-ops; the CMS above to payloadcms-ops; the
Workers runtime below to cloudflare-ops / hono-ops. This skill is the
framework.
Verified against Next.js 16.x (2026-08-30) —
next@16.3.3, docs snapshot 2026-08-25, React 19. Semantics changed materially at 15.0 (fetchuncached by default, request APIs async) and again at 16.0 (Cache Components,proxy.ts,revalidateTagsignature). Establish the app's version before answering any caching question — the right answer for 14 is the wrong answer for 16.
Staleness check: python scripts/check-nextjs-facts.py --offline asserts
the version-bearing facts are still named in the prose and that the currency
note above matches the catalog; --live confirms each package's npm major.
Catalog: assets/nextjs-facts.json.
Three greps, before any advice. Getting this wrong is the single largest source of confidently-wrong Next.js answers.
node -p "require('next/package.json').version" # the major decides everything
grep -rn "cacheComponents" next.config.* # Cache Components on/off
ls proxy.* middleware.* src/proxy.* src/middleware.* 2>/dev/null
| Signal | Model in force | Read |
|---|---|---|
cacheComponents: true | Cache Components — nothing cached unless 'use cache' says so; PPR is the default rendering | references/cache-components.md |
No cacheComponents (16.x default) | Previous model — fetch uncached by default, route-segment config, unstable_cache | references/caching-model.md |
| Next.js ≤ 14 | Legacy — fetch cached by default; most "why is this stale" bugs live here | references/caching-model.md |
What are you doing with Next.js?
│
├─ "It's serving stale data" / "my change doesn't appear"
│ └─ Orient (above) → references/caching-model.md (debug ladder)
│
├─ Deciding what to cache, for how long, and how to bust it
│ └─ references/cache-components.md (use cache, cacheLife, cacheTag)
│ or references/caching-model.md if cacheComponents is off
│
├─ "use client" errors, serialization failures, context, env leaks
│ └─ Below + references/server-client-boundary.md
│
├─ Mutations: Server Action vs Route Handler, auth, validation
│ └─ Below + references/server-actions.md
│
├─ Data fetching, waterfalls, Suspense/loading.tsx, what streams
│ └─ references/data-fetching-streaming.md
│
├─ Routes, async params, layouts, parallel/intercepting routes, metadata
│ └─ references/routing-and-rendering.md
│
├─ proxy.ts (formerly middleware.ts), matchers, edge vs Node runtime
│ └─ references/proxy-and-runtimes.md
│
├─ Shipping it: self-host, Docker, multi-instance, CDN, Cloudflare
│ └─ references/deployment.md
│
├─ Upgrading 14/15 -> 16, or Pages Router -> App Router
│ └─ references/upgrading.md
│
├─ Fonts, scripts, images, bundle size, build speed
│ └─ references/optimization.md
│
├─ Testing it (and what simply cannot be unit-tested)
│ └─ references/testing.md
│
└─ Auditing an existing app for the known footguns
└─ python scripts/audit-app-router.py <project-root>
'use client' marks an entry point into the client module graph. Everything
that file imports — and everything those files import — is bundled for the
browser, whether or not it carries the directive. Components passed through as
children or props are not imported by it, so they stay on the server and
arrive as already-rendered output.
That single asymmetry explains most boundary design:
// ❌ marking the layout client pulls the whole tree into the bundle
'use client'
export default function Layout({ children }) { /* ... */ }
// ✅ keep the layout on the server; make only the interactive leaf a client entry
export default function Layout({ children }) {
return <nav><Logo /><Search /></nav> // Search is the 'use client' file
}
A serialization error at the boundary is the real error. Props crossing
server → client are serialized into the RSC payload; a class instance, a
function, a URL, a Symbol or a Date-bearing ORM row cannot make the trip.
The fix is almost never "wrap it in a Client Component" — it is to stop sending
the un-serializable thing and send the shape the UI renders. That is also the
security fix: returning a raw database row to the client publishes every column
on it.
Two more rules that fall out of the same graph:
{children}, not the tree. A 'use client' provider that
accepts children keeps the subtree on the server. Render it as deep as it can
go so the static parts stay static.NEXT_PUBLIC_* variables reach the
browser; anything else becomes an empty string in a client module — no error,
just a request that fails at runtime with an empty Authorization header.
import 'server-only' turns that into a build error instead. The
client-secret-env rule in audit-app-router.py catches it statically.Depth, interleaving patterns, and the third-party-component wrapper:
references/server-client-boundary.md.
Caching is where Next.js costs teams the most hours, because the defaults inverted between majors and the failure mode is a correct-looking stale page.
The mental model that survives version changes: work is either (a) known at build time, (b) cached with a stated lifetime, or (c) request-time. Every caching bug is one of those three misclassified.
cacheComponents: true)import { cacheLife, cacheTag } from 'next/cache'
async function BlogPosts() {
'use cache' // opt IN — this is the only thing that caches
cacheLife('hours') // ALWAYS state it; omitting it means the implicit 'default'
cacheTag('posts') // the handle you invalidate by
return <List posts={await getPosts()} />
}
Non-negotiables:
cookies(),
headers(), searchParams, and dynamic params throw
next-request-in-use-cache, and the restriction follows the call stack into
helpers. Read them outside and pass the value in as an argument.cacheLife explicitly in every scope. Without it, an inner short-lived
cache can silently shorten the outer one (and, if the outer has no explicit
profile, that combination is a prerender-time build error).'use cache: remote' is the durable,
shared variant, and it costs a network round trip.| Profile | stale (client) | revalidate (server) | expire |
|---|---|---|---|
default | 5 min | 15 min | never |
seconds | 30 s | 1 s | 1 min |
minutes | 5 min | 1 min | 1 hr |
hours | 5 min | 1 hr | 1 day |
days | 5 min | 1 day | 1 week |
weeks | 5 min | 1 week | 30 days |
max | 5 min | 30 days | 1 year |
revalidate: 0 or expire under 5 minutes drops the content out of the
prerender entirely; stale under 30 seconds drops it out of prefetches. Of the
presets only seconds trips either. Full semantics, nesting rules,
use cache: private / remote: references/cache-components.md.
fetch is not cached unless you ask (cache: 'force-cache' or
next: { revalidate: n }); non-fetch work caches via unstable_cache; route
segments are steered by dynamic, revalidate and fetchCache exports. The
layers — request memoization, data cache, full route cache, client router cache
— and the ladder for finding which one is holding the stale value are in
references/caching-model.md.
| API | Semantics | Use when |
|---|---|---|
updateTag(tag) | expires and re-reads in the same response | read-your-own-writes; the user must see their change now. Actions only |
revalidateTag(tag, profile) | stale-while-revalidate; no immediate re-render | shared content that tolerates eventual consistency |
revalidatePath(path) | invalidate one URL | a single route is affected and tagging is overkill |
refresh() | refetch uncached data only, cache untouched | the view depends on state outside the cache. Actions only |
The single-argument revalidateTag('x') form is deprecated in 16 — that is the
revalidate-tag-single-arg finding.
An action is not a function call. 'use server' compiles the implementation
away from the client bundle and leaves an action ID that POSTs back to the
route. Anyone who can send that POST can invoke it, with no form, no page
render, and no UI-level gate in the way.
'use server'
export async function completeItem(itemId: string) {
const session = await auth() // 1. authenticate
if (!session?.user) throw new Error('Unauthorized')
const item = await db.item.findFirst({ // 2. authorize by ownership,
where: { id: itemId, ownerId: session.user.id }, // re-read from a trusted source
})
if (!item) return
await db.item.update({ where: { id: item.id }, data: { completed: true } })
}
proxy.ts matcher — actions are POSTs to the
route they live on, so moving one to another route can silently drop it out of
matcher coverage.Origin-vs-Host CSRF check, a 1MB
body cap, encrypted action IDs, dead-code elimination of unused actions, and
closure-variable encryption. They are a floor, not a substitute.Promise.all over actions is sequential.Deployment, NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, and the "Failed to find Server
Action" skew failure: references/server-actions.md.
The non-obvious ones, in rough order of hours lost.
next dev never caches pages. Development renders on demand, so every
caching bug is invisible until next build && next start. Verify caching
against a production build, or you are testing a different program.next build and fail under
next start. On a dynamically-rendered route the next-request-in-use-cache
error only surfaces when the route actually runs.'use cache' boundary (a cookies() store passed as a prop, a shared Map
of in-flight fetches) is being awaited inside one. It cannot resolve during
prerender. The error names the timeout, not the prop that caused it.await params at the top of a layout de-opts the whole subtree. Awaiting
request data high in the tree shrinks the static shell to nothing. Pass the
promise down and await it inside a <Suspense> boundary instead — the same
move applies to cookies(), headers() and searchParams.proxy.ts without a matcher runs on every request — including
_next/static, _next/image and public/. Auth logic there blocks your own
CSS. Conversely, _next/data still runs proxy even when excluded, on purpose.default.js. Since 16, a @slot
without one fails the build outright.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY; clients
mid-mutation need a retry path, not a stack trace.revalidateTag() only invalidates the instance it ran on. Across pods,
tag state must be synced by a cache handler implementing refreshTags().unstable_cache and 'use cache' are not the same store. use cache
entries are keyed by build id and never survive a deploy — even remote
ones. If something must persist across deploys, it is not a use cache job.Math.random()/Date.now() inside a cached scope freeze one value for
everyone. Call connection() first and wrap in <Suspense> for a
per-request value; cache it deliberately if one shared value is what you want.| Resource | Use it when |
|---|---|
scripts/audit-app-router.py | Auditing or inheriting an app — 16 static rules for the landmines above, gated on the project's detected Next.js major |
scripts/check-nextjs-facts.py | CI / freshness: are this skill's version facts still true? |
assets/next.config.template.ts | Starting a 16.x config, or auditing an inherited one |
assets/nextjs-facts.json | The dated fact catalog the verifier reads |
The audit script takes this skill's own advice: it reads the project's Next.js
major from node_modules/next (or package.json) and suppresses the rules
that postdate it — eight of the sixteen describe breakages introduced in 15 or
16, so running them against a 14-era app would flag correct code. The verdict
line always states the major it gated on. Use --assume-major N when scanning a
bare subdirectory where the version cannot be read.
# Inherit an unfamiliar app: what will bite, worst first
python scripts/audit-app-router.py /path/to/project
# CI gate: block only on the errors
python scripts/audit-app-router.py --min-severity error .
# Machine-readable, for triage or a report
python scripts/audit-app-router.py --json . | jq '.data[] | select(.severity=="error")'
# Scanning a subtree with no package.json in reach
python scripts/audit-app-router.py --assume-major 15 ./packages/web/app
# Is this skill still describing reality?
python scripts/check-nextjs-facts.py --offline
| File | Covers |
|---|---|
| references/caching-model.md | The previous model: four cache layers, defaults per major, the stale-response debug ladder |
| references/cache-components.md | use cache, cache keys, cacheLife/cacheTag, private vs remote, PPR and prefetch |
| references/server-client-boundary.md | use client graph, serialization, interleaving, context, environment poisoning |
| references/server-actions.md | Endpoint model, auth/validation, action vs route handler, config, skew |
| references/data-fetching-streaming.md | Fetch patterns, waterfalls, Suspense/loading.tsx, what a loading state costs |
| references/routing-and-rendering.md | File conventions, async params, dynamic/parallel/intercepting routes, metadata |
| references/proxy-and-runtimes.md | proxy.ts, matchers, execution order, Node vs Edge runtime API gaps |
| references/deployment.md | Self-hosting, Docker, multi-instance, CDN behaviour, the Cloudflare path |
| references/upgrading.md | 14 -> 15 -> 16 deltas (which are silent), Cache Components adoption, Pages -> App |
| references/optimization.md | next/font, next/script strategies, image props, bundle and build speed |
| references/testing.md | The shifted pyramid: async Server Components are E2E-only; action security tests; instant() |
react-ops — React itself: hooks, component architecture, state, the
React-level Server Components model.typescript-ops — the type system behind PageProps/LayoutProps and
strict-mode discipline.tailwind-ops — styling; payloadcms-ops — Payload 3, which is
Next.js-native and inherits every boundary rule here.cloudflare-ops / hono-ops — the Workers runtime, bindings and
Hono-based APIs. Next.js on Cloudflare goes through @opennextjs/cloudflare;
see references/deployment.md for the seam, and those skills for the platform.auth-ops — session and token design that Server Actions depend on;
testing-ops — the test strategy this framework's boundaries need.