Skip to main content

Configuration

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

export default defineConfig({ /* … */ })

defineConfig is an identity function that preserves literal types, so collection names, field keys and locales flow through to the generated types and editor autocomplete. Every key is optional.

Top level

KeyTypeDefaultDescription
contentDirstring'content'Content root, relative to the project root
publicDirstring'public'Next's static directory
defaultLocalestring'en'Locale used when a route has no locale prefix
localesstring[][defaultLocale]Every declared locale. Must contain defaultLocale
autoMetadatabooleanfalseMarks the project metadata-driven — see Page metadata
missingField'empty' | 'key' | 'error''empty'What a field with no value renders
localeFallback'default' | 'empty''default'What a missing translation falls back to
apiPathstring'/api/copy-ink'Mount point of the route handler
adminPathstring'/admin'Sign-in page path
imagesImagesConfigSee below
authAuthConfigSee below
backendBackendConfigSee below
collectionsRecord<string, CollectionSchema>{}See below

contentDir and publicDir are normalised — a leading or trailing / is stripped. apiPath and adminPath are normalised the other way: they always start with / and never end with one.

missingField

ValueProductionEditor
'empty'renders nothinglabelled placeholder
'key'renders the field pathlabelled placeholder
'error'throws MISSING_FIELDthrows

'key' is useful during a content migration, when seeing hero.subtitle on the page is more informative than seeing a gap. 'error' also makes <Copy.Item> throw UNKNOWN_ITEM for an item that does not exist.

images

images: {
allowedHosts: ['cdn.example.com', '*.imgix.net'],
maxUploadBytes: 8 * 1024 * 1024,
}
KeyTypeDefaultDescription
allowedHostsstring[][]Hosts a client may paste a remote image URL from. A leading *. matches one level of subdomain
maxUploadBytesnumber8388608 (8 MB)Uploads above this are rejected at upload time

allowedHosts is written to .copy-ink/image-hosts.json by copy-ink types, for next.config to consume. See Images.

auth

auth: {
provider: 'github',
allowlist: ['jane', 'client@example.com'],
}
KeyTypeDefaultDescription
provider'github''github'Identity provider
allowliststring[][]GitHub logins or email addresses permitted to edit

The allowlist is checked at callback time and again on every editing request, so removing someone takes effect on their next action, not their next sign-in.

An empty allowlist permits nobody. The local auth provider bypasses the check entirely, because there is no identity to check.

backend

backend: {
type: 'github',
repo: 'you/your-site',
branch: 'main',
}
KeyTypeDefaultDescription
type'github' | 'local''github'Where writes go
repostringowner/repo. Required when type is 'github'
branchstring'main'Branch that receives publish commits
strategy'direct''direct'v1 supports direct commits only; branch-and-PR lands in v2

type: 'local' writes to your working tree and, outside production, signs you in automatically. See Auth and backends.

collections

collections: {
posts: {
label: 'Blog posts',
path: '_collections/posts',
slugFrom: 'title',
orderBy: { field: 'date', direction: 'desc' },
fields: { /* … */ },
// route: '/blog/[slug]', // only when the scan cannot work it out
},
}
KeyTypeDefaultDescription
labelstringprettified nameShown in the editor's collection list
pathstringItem directory, relative to the locale's content root
slugFromstringField whose value seeds a new item's slug
routestringderivedRoute rendering one item, e.g. /blog/[slug]
orderBy{ field: string; direction: 'asc' | 'desc' }Default sort
fieldsRecord<string, FieldSchema>The schema

slugFrom must name a field in fields; orderBy.field must too. Both are validated at startup and reported by copy-ink check.

route is what lets the editor open one item from the Content panel. Normally you never write it: copy-ink types finds the route by seeing which page file names the collection. Set it when the scan cannot tell — a route built from more than one dynamic segment, say — or to override what it found. It must start with / and carry exactly one dynamic segment, since it addresses a single item.

Field types

Every field shares four optional keys:

KeyTypeDescription
labelstringEditor label. Defaults to a prettified key name
requiredbooleanBlocks item creation while empty
editablebooleanfalse locks the field in the editor while leaving it readable by code
descriptionstringHelper text under the input

Plus, per type:

typeExtra keysStored as
'text'multiline, maxLengthstring
'richtext'Markdown string
'image'typed node with _type: image
'date'ISO date
'number'min, max, stepnumber
'boolean'boolean
'select'options: (string | { label, value })[]string
fields: {
title: { type: 'text', required: true, maxLength: 70 },
body: { type: 'richtext', description: 'The post itself.' },
status: { type: 'select', options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Live', value: 'live' },
] },
price: { type: 'text', editable: false },
}

Validation

The config is validated when setupCopyInk runs and by copy-ink check. Errors throw CopyInkError('CONFIG_INVALID') with the offending key named. Common ones:

  • defaultLocale is not in locales
  • a collection's slugFrom or orderBy.field is not one of its fields
  • backend.type: 'github' with no repo
  • an allowedHosts entry that is not a hostname
  • a root-level meta key used as content