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:
| Locales | scope | locale | Rendering |
|---|---|---|---|
| One | explicit | — | prerendered |
| Several | explicit | explicit | prerendered |
| Several | explicit | — | dynamic |
| Any | from route | — | dynamic |
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.