Skip to main content

Components

import { Copy } from 'copy-ink'

<Copy field="heroTitle" /> {/* current route */}
<Copy field="hero.subtitle" /> {/* dot path into a namespace */}
<Copy field="heroTitle" scope="about" /> {/* another page */}
<Copy field="companyName" scope="_global" /> {/* site-wide */}
<Copy.Image field="aboutImg" />
<Copy.Rich field="body" />

Copy is both the single-field component and the namespace. Alias the import if the name collides with something in your codebase:

import { Copy as Content } from 'copy-ink'

All of these are async Server Components. They read from the filesystem, so they never make a network call, and they never make a page dynamic on their own.

<Copy> renders text, and only text

In production <Copy field="heroTitle" /> renders the string and nothing else. No wrapper element, no class, no style — nothing that could fight your CSS or change your layout. Put it wherever a string goes:

<h1 className="text-4xl"><Copy field="heroTitle" /></h1>
<p><Copy field="hero.subtitle" /></p>

Non-string values are coerced: numbers and booleans stringify, a typed node or a namespace renders as empty rather than [object Object].

This stays plain-text-only, permanently. Markdown belongs to <Copy.Rich>.

Resolution order

scope (explicit, absolute) > enclosing Copy.Item / Copy.List > current route

scope is always absolute and always escapes an enclosing collection context. One rule, no exceptions — there is no relative form, no ../, and no way for a nested component to inherit half a scope.

The current route comes from the pathname the middleware stamped onto the request, so route groups, rewrites and locale prefixes all resolve correctly.

Scope and rendering

Reading that pathname means reading the request headers, and that opts the page out of static prerendering. Naming a scope skips the lookup:

<Copy field="heroTitle" /> // current route — dynamic
<Copy field="heroTitle" scope="about" /> // explicit — prerenderable

On a multi-locale site an explicit scope is not enough on its own, because the locale still comes from the route. Pin both:

LocalesscopelocaleRendering
Oneexplicitprerendered
Severalexplicitexplicitprerendered
Severalexplicitdynamic
Anyfrom routedynamic

This is a property of the whole tree, the root layout included. A single route-resolving <Copy> in your layout makes every page in the site dynamic, which is easy to miss because the layout is the one file you stop looking at.

<Copy.Item> and <Copy.List> scope their children to the collection rather than the route, so on a single-locale site a collection page prerenders with no extra props at all — pair it with generateStaticParams and the whole /blog/[slug] tree is built ahead of time.

None of this changes what renders, only when. If you are happy serving pages dynamically, ignore it.

The Server Component context boundary

Server Components have no React context. <Copy.Item> and <Copy.List> therefore hand their context to descendants by walking the element tree they receive.

That reaches through host elements and through children you pass to your own components:

<Copy.Item collection="posts" item={slug}>
<article>
<Card>
<Copy field="title" /> {/* ✅ resolves against the item */}
</Card>
</article>
</Copy.Item>

It does not reach into a <Copy> that another component returns from its own body:

function Title() {
return <Copy field="title" /> {/* ❌ resolves against the route */}
}

<Copy.Item collection="posts" item={slug}>
<Title />
</Copy.Item>

Give that one an explicit scope, or pass the field down as a prop. copy-ink check will not silently mislead you here — the static analysis knows the difference between the two shapes.

Choosing a locale

Every component takes an optional locale. Without it the locale comes from the route. Pass it when you deliberately want another one:

<Copy field="heroTitle" locale="de" />

See Localisation.

Editor anchors

With a session present, each component also emits an anchor the editor uses:

<span data-copy-field="heroTitle" data-copy-scope="about">Hello!</span>

Outside editor mode those attributes do not exist. If you are styling against the DOM, style your own elements — never the anchor, which is not there in production.

Beyond JSX

Components cannot fill an alt attribute, a title, or a metadata string. For those, read content directly — see Reading content in code.