Component interface reference
This is the canonical interface for a WiseWig component definition. A component describes what customer data is valid and what the editor may do with it. Its Astro renderer uses the same typed data for published, preview, and edit modes.
TypeScript contract
import { z } from "zod";
import {
defineComponent,
type FieldControl,
type StructuralPermissions,
} from "@wise-wig/contracts";
type ComponentDefinition<T extends z.ZodType> = {
type: string;
content: T;
defaultContent: z.output<T>;
editor: {
fields: Record<string, {
control: FieldControl;
inline: boolean;
formatting?: string[];
}>;
variants?: Record<string, { options: readonly string[] }>;
structure: StructuralPermissions;
placement?: { regions: string[]; maxInstances?: number };
};
migrations?: Array<{
from: number;
migrate: (content: unknown) => unknown;
}>;
ai?: {
description: string;
fieldGuidance?: Partial<Record<keyof z.output<T> & string, string>>;
suggestedActions?: readonly string[];
};
};
declare function defineComponent<T extends z.ZodType>(
definition: ComponentDefinition<T>,
): ComponentDefinition<T>;
The source of truth is packages/contracts/src/index.ts. This reference intentionally mirrors its public shape; update both when the contract changes.
Required members
| Member | Purpose | Rules |
|---|---|---|
type | Stable component discriminator stored in a section document. | Use a portable, unique lowercase slug. Changing it is a breaking content migration. |
content | Zod schema for persisted props. | Make all limits explicit. Validate nested links, assets, repeaters, and discriminated states here. |
defaultContent | Schema-valid initial content. | Must satisfy content; use it for new approved instances and migrations. |
editor.fields | Field-by-field editing affordance. | Only fields named here are offered to the editor UI. Keep its keys aligned to the schema’s top-level editable properties. |
editor.structure | Move/remove/duplicate policy and document-level maximum. | Always state all three booleans; use maxInstances to bound growth. |
editor.variants, editor.placement, migrations, and ai are optional in the type but should normally be explicit for a component that can be inserted into a theme.
migrations declares a version-to-transform mapping for component content. The alpha does not auto-discover or automatically apply custom-component migrations; include an explicit tested migration step when changing a persisted custom schema.
AI semantics
ai explains the component’s intended meaning without changing its content schema or granting an agent permission. description states the component’s job on the page, fieldGuidance gives bounded field-specific writing guidance, and suggestedActions advertises useful operations such as “rewrite for clarity” or “adapt for a target audience.” WiseWig can use these hints to generate MCP prompt context and editor suggestions automatically.
Treat these values as developer-authored instructions, not customer content. They never override Zod limits, editable-field policy, permissions, version checks, confirmation, or the public/private MCP boundary.
Field controls
FieldControl is a closed union. Select the semantic control, not a generic HTML control:
| Control | Intended schema shape |
|---|---|
text | Short plain text, labels, and titles |
rich-text | Bounded text with an explicit formatting allowlist |
image / images | One asset or a bounded asset array |
video | Approved uploaded video asset |
embed | Validated external embed reference, currently approved YouTube normalization |
file | Approved attachment metadata |
link | Validated label/href link object |
repeater | Bounded array of typed objects with stable item IDs |
contact | Contact, hours, or typed location field |
form | Reference to a declared form definition |
inline: true means the overlay may expose an edit affordance in the rendered component. It is a UI hint, not a relaxation of Zod validation or authorization. formatting applies only to controls that render rich text; list only allowed operations, such as bold, italic, and link.
Variants, structure, and placement
Variants are finite developer-approved choices:
variants: {
layout: { options: ["left", "center", "split"] },
}
Do not accept arbitrary class names, CSS values, or layout strings from a customer. A persisted section currently has a variant.layout selected from left, center, or split; a custom renderer must map the selected variant to its own known implementation.
Structure is independent from variants:
structure: {
movable: true,
removable: true,
duplicable: false,
maxInstances: 1,
}
Placement restricts a type to declared theme regions and can apply a region-level cap:
placement: {
regions: ["main", "footer"],
maxInstances: 2,
}
structure.maxInstances limits a type in the page document. placement.maxInstances limits the component at a declared placement boundary. Use the tighter bound when both apply.
Example
import { defineComponent } from "@wise-wig/contracts";
import { z } from "zod";
export const notice = defineComponent({
type: "notice",
ai: {
description: "A time-sensitive customer update with an optional next action.",
fieldGuidance: {
heading: "State the update directly.",
body: "Include the practical impact and any relevant date or deadline.",
},
suggestedActions: ["shorten", "adapt for a target audience"],
},
content: z.object({
heading: z.string().min(1).max(90),
body: z.string().min(1).max(400),
action: z.object({
label: z.string().min(1).max(40),
href: z.string().min(1).max(2_000),
}).optional(),
}),
defaultContent: {
heading: "A useful update",
body: "Add a concise, customer-facing message.",
},
editor: {
fields: {
heading: { control: "text", inline: true },
body: {
control: "rich-text",
inline: true,
formatting: ["bold", "italic", "link"],
},
action: { control: "link", inline: false },
},
variants: { layout: { options: ["left", "center"] } },
structure: {
movable: true,
removable: true,
duplicable: true,
maxInstances: 3,
},
placement: { regions: ["main"], maxInstances: 3 },
},
});
Renderer contract
The definition does not replace a renderer. An Astro renderer receives typed props and a Boolean edit-mode signal, then conditionally adds edit metadata while rendering the same markup and content for every mode:
---
import type { z } from "zod";
import { notice } from "../wise-wig/components/notice.definition.js";
type Props = {
id: string;
props: z.output<typeof notice.content>;
layout: "left" | "center" | "split";
editable?: boolean;
};
const { id, props, layout, editable = false } = Astro.props;
const attrs = editable
? {
"data-edit-section": id,
"data-edit-type": "notice",
"data-editable-fields": "heading,body,action",
tabindex: 0,
}
: {};
---
<section class:list={["notice", `notice--${layout}`]} {...attrs}>
<h2 data-edit-field={editable ? "heading" : undefined}>{props.heading}</h2>
<p data-edit-field={editable ? "body" : undefined}>{props.body}</p>
</section>
Metadata is absent from published output. Do not branch into a separate EditableNotice component and do not serialize rendered markup as content.
Registering a custom component today
The current alpha document schema, renderer dispatch, mutation allowlist, and editor-control registry are closed unions. pnpm wise-wig generate component notice --cwd examples/acme-services creates a definition, Astro renderer, and a checklist, but it does not auto-register the type.
Before accepting the type in a real page document, update and test all of these boundaries together:
- Component registry in
@wise-wig/contracts. - Section discriminated union and any structural/placement validation.
- Astro render dispatch in the consuming theme/site.
- Draft mutation allowlist and editor field-control behavior.
- Theme slot manifest and migration path for existing content.
This deliberate registration work keeps documents, validators, renderers, and editor affordances aligned. See Structural editing, Theme migrations, and Testing and validation.