Troubleshooting
Start here:
npx copy-ink doctor
It checks config, credentials and Next wiring in one pass, and most of what follows is something it already reports.
Nothing renders
A <Copy> renders as empty.
The field is missing from the content file for that scope. Confirm which scope
the route maps to — /about/team is about/team — and run copy-ink check,
which reports missing-key with the file and line of the usage.
Set missingField: 'key' temporarily to see the field path on the page instead
of a gap.
Every <Copy> renders as empty.
The content directory is not where the config says. doctor reports
content-missing.
NOT_SET_UP at request time
Something read content before setupCopyInk() ran. The setup module must be
imported by the module graph that is rendering.
Route handlers are their own entry point — the root layout importing your setup module does not reach them:
import copyInk from '@/copy-ink.setup'
export const { GET, POST } = createCopyInkHandler(copyInk)
doctor reports this as handler-not-wired.
The wrong page's content appears
The middleware is not stamping the pathname, or its matcher excludes the route.
export { copyInkMiddleware as middleware } from 'copy-ink/middleware'
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
If you already had middleware, compose rather than replace — see the quick start.
doctor reports middleware-missing and middleware-not-wired.
A field inside a collection resolves to the route
function Title() {
return <Copy field="title" /> // ❌
}
<Copy.Item collection="posts" item={slug}>
<Title />
</Copy.Item>
Server Components have no React context, so <Copy.Item> hands its context
down by walking the element tree it receives. It cannot reach into what another
component returns from its own body.
Give that <Copy> an explicit scope, or move it into the tree you pass to
<Copy.Item>. See the context
boundary.
The editor never appears
- Is
<CopyInkScripts />rendered in the root layout, inside<body>?doctorwarnsscripts-missing. - Did sign-in actually complete?
GET /api/copy-ink/sessionshould return a user. - Is the account on
auth.allowlist? An empty allowlist permits nobody.
Sign-in loops or fails
"Sign-in could not be verified." The state cookie did not survive the round trip. Usually a callback URL on a different host than the one that started the flow — check the OAuth app's callback matches the deployed domain exactly.
"…is not on the editor allowlist for this site." The GitHub login or email
is not in auth.allowlist. Note that GitHub only returns a public email; use
the login when in doubt.
Signed in, then signed out again on the next request. Sessions live in the
server process. Several instances, or serverless functions that cold-start,
lose them. Supply a shared SessionStore.
Publishing fails
| Message | Code | What to do |
|---|---|---|
| "Someone else changed this page — reload." | STALE_BASE | The branch moved. Reload and redo the edit; there is no merging in v1 |
| A forge error | BACKEND_ERROR | Usually branch protection on backend.branch, or an App without Contents: write |
| "Nothing to publish." | EMPTY | The changeset is empty — every edit was reverted to its original |
For branch protection specifically: the App commits directly, so either exempt
it from the rule or point backend.branch at an unprotected branch.
Images
An image renders as a plain <img> rather than through next/image. Its
host is not in images.allowedHosts. That is a deliberate fallback, not a bug —
declaring the host and restarting next dev fixes it.
Next throws about an unconfigured host. next.config is not importing
.copy-ink/image-hosts.json, or was not restarted after allowedHosts changed.
doctor warns remote-patterns.
An uploaded image 404s. Images live in public/content/…, mirroring the
content tree — not next to the .yml. A relative src resolves against that
mirror.
Types
No autocomplete on field. Run copy-ink types, and add .copy-ink to
your tsconfig include. doctor warns tsconfig-include.
A field I just added is not in the union. The unions are generated. Re-run
copy-ink types. Note that an unknown string still type-checks — the unions are
suggestions, so this never blocks you.
check reports keys I use
Dead-key detection is static. A key read only through getCopy with a computed
path cannot be seen:
npx copy-ink check --no-dead-keys
Missing keys, naming and file-form errors are still reported.
Still stuck
Open an issue with the output of copy-ink doctor and the version of
copy-ink, Next and Node:
github.com/Richard-Sen27/copy-ink/issues.