Creating a custom field
Build a custom field and let Pages CMS register it automatically.
When to create one
Create a custom field when the built-in fields are not enough.
Typical cases:
- a custom picker UI,
- a custom storage format,
- field-specific validation,
- a reusable editing pattern.
Fastest path
Start from a simple existing field such as:
fields/core/booleanfields/core/stringfields/core/select
Avoid starting from a complex field unless you need that behavior.
Folder structure
Create a folder under fields/custom:
fields/
├─ core/
├─ custom/
│ └─ my-field/
│ ├─ edit-component.tsx
│ ├─ view-component.tsx
│ └─ index.tsx
└─ registry.ts
What a field can export
Your index.tsx can export:
labelschemadefaultValuereadwriteEditComponentViewComponent
You usually only need some of them.
What each export does
label
Human-readable field name.
schema
Returns the Zod schema for this field.
defaultValue
Fallback value when there is no stored value and no explicit field default.
read
Transforms stored data into editor data.
write
Transforms editor data back into stored data.
EditComponent
The editing UI. It receives the current value and calls onChange with the next value.
ViewComponent
Compact display used in lists or read-only contexts.
Minimal example
import { z } from "zod";
import { Input } from "@/components/ui/input";
const schema = () => z.string().min(1, "Required");
const EditComponent = ({ value, onChange }: any) => (
<Input value={value || ""} onChange={(event) => onChange(event.target.value)} />
);
const ViewComponent = ({ value }: { value: unknown }) => {
if (!value) return null;
return <span>{String(value)}</span>;
};
const label = "My field";
export { label, schema, EditComponent, ViewComponent };
How registration works
Pages CMS loads fields through fields/registry.ts.
Core fields are registered directly.
Custom fields under fields/custom/<field-type>/index.ts or index.tsx are auto-registered at startup and build time.
The folder name becomes the field type.
fields/custom/my-field/index.tsx
registers:
type: my-field
If you add, remove, or rename a custom field while the dev server is running, restart it.
Test with a minimal config
fields:
- name: promo
type: my-field
Check four things:
- it renders,
- it saves,
- it reloads,
- validation behaves correctly.
When to add read and write
Add them only when the editor value and stored value are different.
Examples:
- editor uses objects but storage uses strings,
- editor uses local dates but storage uses ISO strings,
- editor uses relative media paths but storage wants normalized paths.