Skip to main content

Quick start

This walks a fresh App Router project from nothing to a client-editable site. Steps 1–4 get content rendering in next dev; steps 5–7 turn the editor on.

Already installed the package? Start at step 1. Otherwise see Installation first.

Or run it

npx copy-ink init writes steps 1–5 for you and wires your root layout. Read on anyway — this is what it wrote, and every choice it made for you is here.

1. Configure

copy-ink.config.ts
import { defineConfig } from 'copy-ink/config'

export default defineConfig({
contentDir: 'content',
defaultLocale: 'en',
locales: ['en'],

images: {
allowedHosts: ['cdn.example.com'],
},

auth: {
provider: 'github',
allowlist: ['client@example.com'],
},

backend: {
type: 'github',
repo: 'you/your-site',
branch: 'main',
},

collections: {
posts: {
label: 'Blog posts',
path: '_collections/posts',
slugFrom: 'title',
orderBy: { field: 'date', direction: 'desc' },
fields: {
title: { type: 'text', required: true },
date: { type: 'date', required: true },
cover: { type: 'image' },
excerpt: { type: 'text' },
body: { type: 'richtext' },
},
},
},
})
Start local

backend: { type: 'local' } writes edits straight to your working tree and needs no credentials at all. Switch to the GitHub backend once the site is wired up — see Auth and backends.

2. Register the config once per process

copy-ink.setup.ts
import { setupCopyInk } from 'copy-ink/server'
import config from './copy-ink.config'

export default setupCopyInk(config)

This must run at module scope, from a module your root layout imports — not during render. Keep the returned runtime as the default export: the route handler in step 5 needs it.

3. Wire the root layout

app/layout.tsx
import { CopyInkScripts } from 'copy-ink/server'
import '../copy-ink.setup'

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<CopyInkScripts />
</body>
</html>
)
}

<CopyInkScripts /> renders nothing at all for a visitor. Only once a session exists does it render a small loader that pulls the editor in as its own lazily-imported chunk.

4. Add the middleware

middleware.ts
export { copyInkMiddleware as middleware } from 'copy-ink/middleware'

export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

This stamps the request pathname onto a header so Server Components can resolve the current route. usePathname() is deliberately not used: it is unavailable on the server and breaks with route groups, rewrites and locale prefixes.

Already have middleware? Compose it:

middleware.ts
import { createCopyInkMiddleware } from 'copy-ink/middleware'

export const middleware = createCopyInkMiddleware({
before: (request) => yourOwnLogic(request),
})

At this point content renders. Add a <Copy> to a page and check it:

app/page.tsx
import { Copy } from 'copy-ink'

export default function Page() {
return <h1><Copy field="heroTitle" /></h1>
}

5. Mount the route handler and the sign-in page

app/api/copy-ink/[...copyInk]/route.ts
import { createCopyInkHandler } from 'copy-ink/server'
import copyInk from '@/copy-ink.setup'

export const { GET, POST } = createCopyInkHandler(copyInk)
export const runtime = 'nodejs'

Pass the runtime explicitly. A route handler is its own entry point with its own module graph, so the root layout importing the setup module does not register anything here — and the failure would show up at request time, not at build.

app/admin/page.tsx
export { AdminPage as default } from 'copy-ink/admin'

6. Wire remote image hosts

next.config.mjs
import hosts from './.copy-ink/image-hosts.json' with { type: 'json' }

export default {
images: {
remotePatterns: hosts.map((hostname) => ({ protocol: 'https', hostname })),
},
}

Generate that file with npx copy-ink types. next.config is read at process start, so next dev needs a restart when allowedHosts changes.

Skip this step if every image is a local upload.

7. Check your wiring

npx copy-ink doctor

doctor reads your config, the environment and your Next files, then reports what is missing: no middleware, a handler mounted without its runtime, a next.config that never imports image-hosts.json, absent credentials. Fix what it reports and you are done.

Where to go next