Components
import { Copy } from 'copy-ink'
Copy is the single-field component and the namespace for the rest. All five
are async Server Components.
Shared props
Every field-reading component (<Copy>, <Copy.Rich>, <Copy.Image>) takes:
| Prop | Type | Description |
|---|---|---|
field | string | Dot path into the content file. Required |
scope | string | Absolute scope. Overrides the route and any enclosing item |
locale | string | Overrides the route's locale |
scope is always absolute and always escapes an enclosing collection context.
See resolution order.
Both props also decide how the page renders: without them the component resolves the current route, which reads the request headers and makes the page dynamic. See scope and rendering.
<Copy>
<Copy field="heroTitle" />
<Copy field="hero.subtitle" scope="about" locale="de" />
Renders a plain string, and in production nothing else — no element, no class, no style. Numbers and booleans stringify; anything else renders as empty.
<Copy.Rich>
<Copy.Rich field="body" />
Renders Markdown to React elements against a strict allowlist. No HTML string is produced. See Rich text.
Shared props only.
<Copy.Image>
<Copy.Image field="cover" className="rounded" sizes="(max-width: 768px) 100vw, 50vw" priority />
Wraps next/image, taking src, alt, width and height from the typed
node. In addition to the shared props:
| Prop | Type |
|---|---|
className | string |
sizes | string |
priority | boolean |
quality | number |
loading | 'lazy' | 'eager' |
A field that is not an image node renders nothing in production, and a labelled
placeholder in editor mode. A remote source from an undeclared host renders as
a plain <img unoptimized>.
<Copy.Item>
<Copy.Item collection="posts" item={slug}>
<h1><Copy field="title" /></h1>
</Copy.Item>
| Prop | Type | Description |
|---|---|---|
collection | string | Collection name from the config. Required |
item | string | Item slug. Required |
locale | string | Overrides the route's locale |
children | ReactNode | Scoped to the item. Required |
Falls back to the default locale's item when the current locale has none.
Renders nothing when the item does not exist, unless missingField is
'error', which throws UNKNOWN_ITEM.
<Copy.List>
<Copy.List collection="posts" limit={10} orderBy="date" direction="desc">
{(item, index) => <article key={item.slug}>…</article>}
</Copy.List>
| Prop | Type | Description |
|---|---|---|
collection | string | Collection name. Required |
children | (item, index) => ReactNode | Render function. Required |
limit | number | Maximum items, applied after ordering |
orderBy | string | Field to sort by. Defaults to the schema's orderBy |
direction | 'asc' | 'desc' | Sort direction |
filter | (item) => boolean | Applied after ordering |
empty | ReactNode | Rendered when nothing is visible |
locale | string | Overrides the route's locale |
Drafts are hidden outside editor mode. Each row is scoped to its own item.
CollectionItem
The value passed to the render function, and returned by getItem / getList:
interface CollectionItem {
collection: string
slug: string
locale: string
/** Repo-relative path of the item's YAML file. */
path: string
data: ContentData
draft: boolean
/** Manual position written by the editor's reorder control. */
order: number | null
}
<CopyInkScripts />
import { CopyInkScripts } from 'copy-ink/server'
<body>
{children}
<CopyInkScripts />
</body>
Renders nothing at all without a session. With one, renders the loader that
dynamically imports the editor. Takes no props. Render it once, in the root
layout, inside <body>.
<CopyProvider>
import { CopyProvider } from 'copy-ink/server'
<CopyProvider scope="about">{children}</CopyProvider>
| Prop | Type | Description |
|---|---|---|
scope | string | Absolute. Defaults to the current route |
locale | string | Defaults to the route's locale |
children | ReactNode | Required |
Publishes one scope's content to client components so
useCopy works. Put it in the page, not the root layout,
when it should follow navigation.
AdminPage
export { AdminPage as default } from 'copy-ink/admin'
The sign-in page, mounted at adminPath. Signed out it offers a sign-in
button; signed in it names the user and links back to the site.
It reads ?next= from searchParams and returns there after sign-in, so a
"edit this page" link can send the client straight back where they were.