Reading content in code
Components cannot fill an alt attribute, a title, a metadata string or a
JSON response. For those, read content directly.
In Server Components
import { getCopy, getItem, getList, getSlugs, getMetadata } from 'copy-ink/server'
const copy = await getCopy({ scope: 'about', locale: 'de' })
copy.get('hero.subtitle') // string, '' when missing
copy.raw('aboutImg') // the parsed value, including typed nodes
copy.has('hero.subtitle') // boolean
copy.all() // the whole parsed document
copy.scope // 'about'
copy.locale // 'de'
Everything is on the filesystem, so none of this makes a network call.
get coerces: strings pass through, numbers and booleans stringify, anything
else reads as ''. Reach for raw when you need the structure.
Scope and dynamic rendering
Without a scope, getCopy resolves the current route, which means reading the
request headers — and that makes the page dynamic.
await getCopy() // current route, dynamic
await getCopy({ scope: 'about' }) // explicit, stays statically prerenderable
Pass an explicit scope in anything you want prerendered.
Collections
const post = await getItem('posts', slug)
const posts = await getList('posts', { limit: 5, orderBy: 'date', direction: 'desc' })
const slugs = await getSlugs('posts')
getList takes the same options as <Copy.List> and orders items the same way.
Drafts are excluded. getSlugs is the shape generateStaticParams wants:
export async function generateStaticParams() {
return (await getSlugs('posts')).map((slug) => ({ slug }))
}
In Client Components
'use client'
import { useCopy } from 'copy-ink/client'
export function Hero() {
const copy = useCopy()
return <img src="/hero.png" alt={copy.get('heroImg.alt')} />
}
useCopy needs a provider in scope:
import { CopyProvider } from 'copy-ink/server'
export default function Page() {
return (
<CopyProvider>
<Hero />
</CopyProvider>
)
}
Put the provider in the page rather than the root layout when it should follow navigation — App Router does not re-render a layout that did not change, so a layout-level provider would keep serving the first route's content.
useCopy is also exported from copy-ink, but importing it from
copy-ink/client guarantees a client component pulls in nothing but the hook,
without relying on the bundler to tree-shake.
The provider serialises one scope's content into the RSC payload. Keep it to
pages that need it; a _global scope full of copy is a page-weight decision.
In route handlers
Same API, no provider needed:
import { getCopy } from 'copy-ink/server'
export async function GET() {
const copy = await getCopy({ scope: '_global' })
return Response.json({ company: copy.get('companyName') })
}
A route handler is its own entry point, so import your setup module there too:
import '@/copy-ink.setup'
Typed reads
copy-ink types generates unions for scopes, fields, collections and locales,
and the API accepts them. They are suggestions, not constraints — string stays
assignable, so a stale manifest never turns a working project red. See
Generated types.