You're reading docs for Nextly Alpha. APIs may change between releases.

Configuration

Components

Define reusable field groups that can be shared across collections and singles.

Components are reusable field-group templates. Define a set of fields once as a component, then embed it in any number of collections or singles. Each usage creates a separate data instance — components are schemas, not shared documents.

Source of truth: the ComponentConfig interface lives in packages/nextly/src/components/config/types.ts. The defineComponent() helper is in packages/nextly/src/components/config/define-component.ts.

Key characteristics:

  • Templates, not documents — components define a field structure; each embed creates its own data row.
  • Own database table — each component type gets a table prefixed with comp_ (e.g. comp_seo).
  • Nesting — a component's fields can include component fields referencing other components (max depth: 3 levels).
  • Slug uniqueness — component slugs must be unique across components, collections, and singles.
  • Dual creation — define in code with defineComponent() or visually in the Visual Schema Builder.

Use defineComponent() from nextly to create components in TypeScript:

src/components/seo.ts
import {
  defineComponent,
  text,
  textarea,
  upload,
} from "nextly";

export default defineComponent({
  slug: "seo",
  label: { singular: "SEO Metadata" },
  admin: {
    category: "Shared",
    icon: "Search",
    description: "Search engine optimization metadata",
  },
  fields: [
    text({ name: "metaTitle", required: true, label: "Meta Title" }),
    textarea({
      name: "metaDescription",
      label: "Meta Description",
      maxLength: 160,
    }),
    upload({ name: "ogImage", relationTo: "media", label: "OG Image" }),
    text({ name: "canonicalUrl", label: "Canonical URL" }),
  ],
});

Components can also be created visually in the admin UI:

  1. Navigate to Components in the sidebar.
  2. Click Create Component.
  3. Name it and add fields using the drag-and-drop Schema Builder.
  4. Save — the component is immediately available for use.

Builder-created components work identically to code-defined ones; both produce the same database schema and API behavior.

Component options

Only slug and fields are required.

OptionTypeRequiredDescription
slugstringYesUnique identifier across components, collections, and singles. Used as the DB table prefix (comp_{slug}). Must be URL-friendly.
fieldsFieldConfig[]YesArray of field definitions.
label{ singular: string }NoDisplay name in the admin UI. Auto-generated from slug if omitted (e.g. social-linkSocial Link).
admin.categorystringNoCategory that groups components in the sidebar and selector modal (e.g. "Shared", "Blocks").
admin.iconstringNoLucide icon name shown in sidebar and component selector.
admin.descriptionstringNoHelp text in the component selector modal.
admin.hiddenbooleanNoHide from admin navigation. Still usable in code and via API.
admin.imageURLstringNoPreview image URL shown in the component selector.
dbNamestringNoCustom database table name (overrides comp_{slug}).
descriptionstringNoGeneral description; falls back to admin.description.
customRecord<string, unknown>NoArbitrary metadata for plugins or custom code. Not persisted.

Components do not have hooks or access control of their own. They inherit hook execution and access checks from the collection or single they're embedded in.

Using components in collections and singles

Once defined, embed a component in any collection or single using the component field helper. There are three usage modes.

Single component (fixed type)

Embed exactly one instance of a specific component type:

src/collections/pages.ts
import { defineCollection, component, text, richText } from "nextly";

export default defineCollection({
  slug: "pages",
  fields: [
    text({ name: "title", required: true }),
    richText({ name: "content" }),
    component({ name: "seo", component: "seo" }),
  ],
});

Dynamic zone (multiple component types)

Let editors choose from several component types — ideal for flexible page builders:

src/collections/pages.ts
import { defineCollection, component, text } from "nextly";

export default defineCollection({
  slug: "pages",
  fields: [
    text({ name: "title", required: true }),
    component({
      name: "layout",
      components: ["hero", "cta", "content-block", "image-gallery"],
      repeatable: true,
    }),
  ],
});

Repeatable single component

An array of the same component type — for example, a list of feature cards:

src/collections/landing-pages.ts
import { defineCollection, component, text } from "nextly";

export default defineCollection({
  slug: "landing-pages",
  fields: [
    text({ name: "title", required: true }),
    component({
      name: "features",
      component: "feature-card",
      repeatable: true,
      minRows: 1,
      maxRows: 12,
    }),
  ],
});

The component field's full option reference lives in Fields → Component.

Example: hero section component

src/components/hero.ts
import {
  defineComponent,
  text,
  upload,
  select,
  option,
} from "nextly";

export default defineComponent({
  slug: "hero",
  label: { singular: "Hero Section" },
  admin: {
    category: "Blocks",
    icon: "Image",
    description: "Full-width hero banner with heading and CTA",
  },
  fields: [
    text({ name: "heading", required: true, label: "Heading" }),
    text({ name: "subheading", label: "Subheading" }),
    upload({
      name: "backgroundImage",
      relationTo: "media",
      label: "Background Image",
    }),
    text({ name: "ctaText", label: "CTA Button Text" }),
    text({ name: "ctaLink", label: "CTA Button Link" }),
    select({
      name: "alignment",
      label: "Content Alignment",
      options: [option("Left"), option("Center"), option("Right")],
      defaultValue: "center",
    }),
  ],
});

Component nesting

Components can embed other components using the component field type, up to 3 levels deep. defineConfig() rejects circular references and configurations that exceed the depth limit at startup.

src/components/faq-item.ts
import { defineComponent, text, component } from "nextly";

export default defineComponent({
  slug: "faq-item",
  label: { singular: "FAQ Item" },
  fields: [
    text({ name: "question", required: true }),
    text({ name: "answer", required: true }),
    component({ name: "cta", component: "cta" }),
  ],
});

Next steps

  • Fields — all field types available inside components
  • Collections — where components are most commonly used
  • Singles — embed components in single-document content