XSite Add New Component Variant
This guide explains the complete workflow to create, generate, publish, and register a new component variant in XSite.
Schemas
packages/schemas is the JSON source of truth (SOT) for schema authoring.
Edit JSON files in src/source/**, then generate Strapi and Puck-compatible outputs.
Commands
pnpm run validate- validate source JSON onlypnpm run generate- validate and generate outputspnpm run lock:components- synccomponent-ids.lock.jsonfrom component filespnpm run rename:component -- --old "shared.button" --new "shared.primary-button"- safely rename a component and update refs + lock filepnpm run generate:types- generate TypeScript interfaces from the compiled schema cache produced bygenerate
Source Layout
src/source/version.jsonsrc/source/component-ids.lock.jsonsrc/source/components/**/*.jsonsrc/source/collections/**/*.jsonsrc/sot/*.schema.json(AJV validation schemas)
Code Structure
src/cli- command entrypoints (validateandgenerate)src/core- shared types, source compiler, normalizationsrc/validation- AJV and compiled-schema validationsrc/transformers/strapi- Strapi output transformersrc/scripts- maintenance scripts (lock:components,rename:component)
src/source/version.json
{
"$schema": "../sot/version.schema.json",
"version": "1"
}Component Authoring Guideline
Use one file per component.
{
"$schema": "../../../sot/component.schema.json",
"id": "dynamic-zone.about",
"title": "About Us",
"description": "Optional",
"fields": {
"cmp_variant": "string!",
"title": "string",
"buttons": "component(shared.button)[]",
"image": "media(images)"
}
}Component keys:
idrequired, format must becategory.nametitleoptional display namedescriptionoptionalfieldsrequired object
Important:
- Always include
cmp_variantin every component. - Missing
cmp_variantcan break installation and registry behavior in new projects.
Collection Authoring Guideline
Use one file per collection or single type.
{
"$schema": "../../sot/collection.schema.json",
"id": "page",
"mode": "collection",
"title": "Pages",
"draft": true,
"i18n": true,
"fields": {
"seo": "component(shared.seo)",
"slug": "uid(title)!",
"dynamic_zone": "dynamicZone(dynamic-zone.hero,dynamic-zone.cta)"
}
}Collection keys:
idrequired, collection namemodeoptional:collection(default) orsingletitleoptionaldescriptionoptionaldraftoptional booleani18noptional booleanfieldsrequired object
Important:
- Add
slugfields for collections where wrapper/detail fetch depends on slug.
Field Guideline (DX)
You can use shorthand or object form.
- Shorthand is fast for simple fields.
- Object form is better for long-term maintainability when options are likely to grow.
Shorthand Quick Reference
- Scalars:
string,text,blocks,richtext,email,password,date,time,datetime,timestamp,integer,biginteger,float,decimal,boolean,json - Required: append
!, for examplestring! - UID:
uidoruid(title)with optional! - Component:
component(shared.button)and repeatablecomponent(shared.button)[] - Dynamic zone:
dynamicZone(dynamic-zone.hero,dynamic-zone.cta) - Media:
media,media(images,files),media(images)[] - Relation:
relation(oneToOne,api::author.author),relation(oneToMany,api::comment.comment,inversedBy=post),relation(manyToOne,api::author.author,mappedBy=posts) - Enum:
enum(draft,review,published)
Relation Shorthand Details
Format:
relation(<relationType>,<target>[,<optionKey>=<optionValue>])
Supported relation types:
oneToOne,oneToMany,manyToOne,manyToMany
Supported options:
inversedBy=<fieldName>mappedBy=<fieldName>- Aliases:
inv,ib,inversemap toinversedBy;map,mb,mappedmap tomappedBy
Rules:
- Use only one of
inversedByormappedByin shorthand. !is supported as with other shorthand fields, for examplerelation(oneToOne,api::author.author)!.- Option format supports
key=value,key:value, andkey(value).
Recommended concise forms:
relation(oneToMany,api::comment.comment,inv=post)relation(manyToOne,api::post.post,map=comments)relation(oneToMany,api::comment.comment,ib:post)relation(manyToOne,api::post.post,mb(comments))
Usage note:
- Keep relation type and target explicit; shorten only option keys for readability.
Object Form (Preferred for Evolving Fields)
{
"type": "component",
"component": "shared.button",
"repeatable": true,
"required": false
}Common keys:
required,unique,default,private,configurable,localizedvalidation:min,max,minLength,maxLength,regexui:label,placeholder,help,hidden,readonly,widget,group,width
Type-specific keys:
uid->targetFieldenumeration->enummedia->multiple,allowedTypesrelation->relation,target,inversedBy,mappedBycomponent->component,repeatabledynamicZone->componentscustomField->customField,options
Required and Repeatable Behavior
string!means required stringcomponent(x)means non-repeatable componentcomponent(x)[]means repeatable component arraymedia(images)means single mediamedia(images)[]means multiple mediadynamicZone(...)always represents a list of allowed component blocks
Type Safety and Error Prevention
Validation is done in three layers:
- SOT JSON Schema (AJV)
- Compiled schema validation
- Component ID lock validation
What these checks enforce:
- Correct file shape and key/value types
- Correct
$schemapaths - Duplicate ID detection
- Unknown component reference detection in
component(...) - Unknown component reference detection in
dynamicZone(...) - Lock consistency against
src/source/component-ids.lock.json
This provides fail-fast, file-level errors.
Generated Output
Generation writes to:
apps/strapi/src/api/**/content-types/**/schema.jsonapps/strapi/src/api/**/controllers/*.tsapps/strapi/src/api/**/routes/*.tsapps/strapi/src/api/**/services/*.tsapps/strapi/src/components/**/*.json
Future-Proof DX Recommendations
- Start with shorthand for simple primitives.
- Use object form for fields likely to need options later.
- Keep component references stable (
category.name). - Run
pnpm run lock:componentsafter adding/removing component IDs. - Use
pnpm run rename:component -- --old "<old>" --new "<new>"for renames.
Process to Add New Component Variants
1) Create SOT files
Create schema JSON files in the correct source folders:
- Collections in the collections source path
- Components/shared components in the components source path
2) Lock component IDs
Run:
pnpm run lock:componentsThis keeps component IDs stable and prevents future mismatch issues.
3) Generate schemas for Strapi
Run:
pnpm generate:schemasThis generates Strapi-ready schema artifacts under apps/strapi.
4) Generate types
Run:
pnpm run generate:typesThis generates interfaces/types and updates categories used by plop flows.
5) Generate Strapi and global registries
After SOT updates, run:
pnpm registry:strapi
pnpm registry:globalThis updates registry outputs and makes components available in generation flows.
6) Scaffold new component via plop
Run:
pnpm plopYou can choose from category groups such as Single and Collection, then pick the specific variant group.
Naming convention:
hero1,hero2,hero3header1,header2
Pattern:
{category}{number}7) Confirm client or server component
If the component uses useState, useEffect, browser APIs, event handlers, or interactive logic, it must be a client component.
Update the tsup config for client output:
banner: {
js: '"use client";',
},
esbuildOptions(options) {
options.banner = {
js: '"use client";',
};
},
format: ["cjs", "esm"],8) Update meta.ts
Ensure metadata is complete and consistent.
import { ComponentMetaInterface } from "@interfaces/registry/meta";
import { data } from "./data";
import Schema from "./schema.json";
export const meta: ComponentMetaInterface = {
key: "hero:hero1",
name: "hero1",
category: "hero",
type: "dynamic-zone",
kind: "component",
description:
"A classic hero section with primary heading, subtitle and call-to-action support. Ideal for homepages and marketing landing pages.",
previewImage:
"https://xsite-components.s3.ap-south-1.amazonaws.com/hero/hero1.png",
tags: ["hero", "landing", "homepage", "marketing", "dynamic-zone"],
props: data,
schema: Schema,
availability: {
plans: ["free", "pro"],
},
};Meta checklist:
keyis unique and usescategory:namenamematches the component package variantcategorymatches the component grouptypeis correct (dynamic-zoneorglobal)previewImageis validtagsare searchablepropshas sensible default dataschemapoints to the correct JSON schemaavailability.plansis set correctly
Note:
- If the list contains only list pages, do not add list type on parts.
9) Publish workflow (changesets -> staging -> production)
Create changeset:
npx changesetChoose bump type:
patchfor fixesminorfor new featuresmajorfor breaking changes
Apply version bump:
npx changeset versionPublish to staging (Verdaccio):
pnpm publish:staging @xsite/ui-hero1Promote to production:
pnpm promote:production @xsite/ui-hero1@1.0.210) Sync dependency in xsite-builder
After publishing in ui-builder, add the package in xsite-builder/package.json.
Example:
{
"@xsite/ui-about1": "staging",
"@xsite/ui-about2": "staging",
"@xsite/ui-blog1": "staging",
"@xsite/ui-contact1": "staging",
"@xsite/ui-cta1": "staging",
"@xsite/ui-cta2": "staging"
}Install latest staging dependencies:
pnpm sync:deps -- --staging
pnpm installComponent Editor Schema Setup
Add editor schema only for newly introduced variants. Do not duplicate schema files for variants that are already supported.
Example path:
docs/schemas/contact.schema.jsonExample contact.schema.json
{
"title": "Contact Component",
"type": "object",
"required": [],
"properties": {
"title": { "title": "Title", "type": "string" },
"subtitle": { "title": "Subtitle", "type": "string" },
"description": { "title": "Description", "type": "string" },
"contactInfo": {
"title": "Contact Info",
"$ref": "#/definitions/contactInfo"
},
"form": { "title": "Form", "$ref": "#/definitions/form" }
},
"definitions": {
"contactInfo": {
"type": "object",
"required": ["heading", "sub_heading", "items"],
"properties": {
"heading": { "type": "string" },
"sub_heading": { "type": "string" },
"items": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/contactItem" }
}
}
},
"contactItem": {
"type": "object",
"required": ["label", "value"],
"properties": {
"label": { "type": "string" },
"value": { "type": "string" },
"link": { "type": "string" }
}
},
"form": {
"type": "object",
"required": [
"heading",
"sub_heading",
"fields",
"button",
"successMessage",
"errorMessage"
],
"properties": {
"heading": { "type": "string" },
"sub_heading": { "type": "string" },
"fields": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/formField" }
},
"button": { "$ref": "#/definitions/button" },
"successMessage": { "type": "string" },
"errorMessage": { "type": "string" }
}
},
"formField": {
"type": "object",
"required": ["name", "label", "type"],
"properties": {
"name": { "type": "string" },
"label": { "type": "string" },
"type": {
"type": "string",
"enum": ["text", "email", "tel", "textarea"]
},
"placeholder": { "type": "string" },
"required": { "type": "boolean" }
}
},
"button": {
"type": "object",
"required": ["label", "path", "target", "variant", "size"],
"properties": {
"label": { "type": "string" },
"path": { "type": "string" },
"target": { "type": "string", "enum": ["_self", "_blank"] },
"variant": { "type": "string" },
"size": { "type": "string" }
}
}
}
}Load Schemas in component-schema-loader.ts
Register new schema imports after adding schema files:
export const componentSchemaLoader: Record<
string,
() => Promise<{ default: JsonSchema }>
> = {
about: () => import("docs/schemas/About/about.schema.json"),
hero: () => import("docs/schemas/Hero/hero1.schema.json"),
cta: () => import("docs/schemas/cta.schema.json"),
faq: () => import("docs/schemas/faq.schema.json"),
testimonial: () => import("docs/schemas/testimonial.schema.json"),
pricing: () => import("docs/schemas/pricing.schema.json"),
partners: () => import("docs/schemas/partners.schema.json"),
wrapper: () => import("docs/schemas/wrapper.schema.json"),
team: () => import("docs/schemas/team.schema.json"),
contact: () => import("docs/schemas/contact.schema.json"),
"page-header": () => import("docs/schemas/page-header.schema.json"),
stat: () => import("docs/schemas/stat.schema.json"),
process: () => import("docs/schemas/process.schema.json"),
};This enables editor form generation for component data.
Final Verification Checklist
-
cmp_variantadded in every component schema -
slugadded in required collections -
pnpm run validatepasses -
pnpm run generatepasses -
pnpm run lock:componentscompleted -
pnpm registry:strapiandpnpm registry:globalcompleted - Component generated via
pnpm plop - Client/server behavior verified and
tsupupdated if needed -
meta.tsupdated correctly -
npx changesetcreated with correct bump type -
npx changeset versionapplied - Published to staging and promoted to production
- Dependency added in
xsite-builder/package.json -
pnpm sync:deps -- --stagingandpnpm installcompleted - New editor schema added
-
component-schema-loader.tsupdated
Golden Rules
- Never skip
cmp_variant. - Always regenerate registries after schema changes.
- Always use changesets for versioning.
- Publish to staging first, then promote to production.
- Keep
xsite-builderdependencies in sync. - Add and register editor schema for every new variant.