Components
Add New Componets

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 only
  • pnpm run generate - validate and generate outputs
  • pnpm run lock:components - sync component-ids.lock.json from component files
  • pnpm run rename:component -- --old "shared.button" --new "shared.primary-button" - safely rename a component and update refs + lock file
  • pnpm run generate:types - generate TypeScript interfaces from the compiled schema cache produced by generate

Source Layout

  • src/source/version.json
  • src/source/component-ids.lock.json
  • src/source/components/**/*.json
  • src/source/collections/**/*.json
  • src/sot/*.schema.json (AJV validation schemas)

Code Structure

  • src/cli - command entrypoints (validate and generate)
  • src/core - shared types, source compiler, normalization
  • src/validation - AJV and compiled-schema validation
  • src/transformers/strapi - Strapi output transformer
  • src/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:

  • id required, format must be category.name
  • title optional display name
  • description optional
  • fields required object

Important:

  • Always include cmp_variant in every component.
  • Missing cmp_variant can 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:

  • id required, collection name
  • mode optional: collection (default) or single
  • title optional
  • description optional
  • draft optional boolean
  • i18n optional boolean
  • fields required object

Important:

  • Add slug fields 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 example string!
  • UID: uid or uid(title) with optional !
  • Component: component(shared.button) and repeatable component(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, inverse map to inversedBy; map, mb, mapped map to mappedBy

Rules:

  • Use only one of inversedBy or mappedBy in shorthand.
  • ! is supported as with other shorthand fields, for example relation(oneToOne,api::author.author)!.
  • Option format supports key=value, key:value, and key(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, localized
  • validation: min, max, minLength, maxLength, regex
  • ui: label, placeholder, help, hidden, readonly, widget, group, width

Type-specific keys:

  • uid -> targetField
  • enumeration -> enum
  • media -> multiple, allowedTypes
  • relation -> relation, target, inversedBy, mappedBy
  • component -> component, repeatable
  • dynamicZone -> components
  • customField -> customField, options

Required and Repeatable Behavior

  • string! means required string
  • component(x) means non-repeatable component
  • component(x)[] means repeatable component array
  • media(images) means single media
  • media(images)[] means multiple media
  • dynamicZone(...) always represents a list of allowed component blocks

Type Safety and Error Prevention

Validation is done in three layers:

  1. SOT JSON Schema (AJV)
  2. Compiled schema validation
  3. Component ID lock validation

What these checks enforce:

  • Correct file shape and key/value types
  • Correct $schema paths
  • 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.json
  • apps/strapi/src/api/**/controllers/*.ts
  • apps/strapi/src/api/**/routes/*.ts
  • apps/strapi/src/api/**/services/*.ts
  • apps/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:components after 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:components

This keeps component IDs stable and prevents future mismatch issues.

3) Generate schemas for Strapi

Run:

pnpm generate:schemas

This generates Strapi-ready schema artifacts under apps/strapi.

4) Generate types

Run:

pnpm run generate:types

This 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:global

This updates registry outputs and makes components available in generation flows.

6) Scaffold new component via plop

Run:

pnpm plop

You can choose from category groups such as Single and Collection, then pick the specific variant group.

Naming convention:

  • hero1, hero2, hero3
  • header1, 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:

  • key is unique and uses category:name
  • name matches the component package variant
  • category matches the component group
  • type is correct (dynamic-zone or global)
  • previewImage is valid
  • tags are searchable
  • props has sensible default data
  • schema points to the correct JSON schema
  • availability.plans is 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 changeset

Choose bump type:

  • patch for fixes
  • minor for new features
  • major for breaking changes

Apply version bump:

npx changeset version

Publish to staging (Verdaccio):

pnpm publish:staging @xsite/ui-hero1

Promote to production:

pnpm promote:production @xsite/ui-hero1@1.0.2

10) 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 install

Component 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.json

Example 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_variant added in every component schema
  • slug added in required collections
  • pnpm run validate passes
  • pnpm run generate passes
  • pnpm run lock:components completed
  • pnpm registry:strapi and pnpm registry:global completed
  • Component generated via pnpm plop
  • Client/server behavior verified and tsup updated if needed
  • meta.ts updated correctly
  • npx changeset created with correct bump type
  • npx changeset version applied
  • Published to staging and promoted to production
  • Dependency added in xsite-builder/package.json
  • pnpm sync:deps -- --staging and pnpm install completed
  • New editor schema added
  • component-schema-loader.ts updated

Golden Rules

  1. Never skip cmp_variant.
  2. Always regenerate registries after schema changes.
  3. Always use changesets for versioning.
  4. Publish to staging first, then promote to production.
  5. Keep xsite-builder dependencies in sync.
  6. Add and register editor schema for every new variant.