技能 编程开发 网站构建器组件开发与集成

网站构建器组件开发与集成

v20260507
webiny-website-builder
本技能指南详细介绍如何利用SDK为网站构建器平台开发和集成自定义组件。开发者可以学习组件的注册流程,定义各种配置化输入类型(如文本、图片、颜色等),处理复杂的数据模型,实现主题定制,并构建Server Components来获取CMS数据。适用于构建具有专业、可编辑功能的网站编辑器。
获取技能
451 次下载
概览

Website Builder

TL;DR

The Webiny Website Builder uses a unique architecture: the Admin editor loads your Next.js app inside an iframe. All component code and styles live in your Next.js project -- Webiny only stores the page structure (which components and what input values). You build editor components with @webiny/website-builder-nextjs, register them via createComponent(), define configurable inputs, and manage theming through CSS custom properties and createTheme().

Architecture

+----------------------------------------------------------+
|  Webiny Admin                                            |
|  +----------------------------------------------------+  |
|  |  Website Builder Editor                            |  |
|  |                                                    |  |
|  |   sidebar     +------------------------------+     |  |
|  |   (inputs)    |  your Next.js app (iframe)   |     |  |
|  |               |  real components             |     |  |
|  |               |  real styles                 |     |  |
|  |               +------------------------------+     |  |
|  +----------------------------------------------------+  |
+----------------------------------------------------------+
                        postMessage (SDK)
+----------------------------------------------------------+
|  Your Next.js App (running separately)                   |
|  @webiny/website-builder-nextjs SDK installed            |
+----------------------------------------------------------+

Key implications:

  • No style clashes -- your components, your styles, full ownership
  • Genuine WYSIWYG -- editors see your real app, not a simulation
  • Framework-owned code -- all React components live in your Next.js repo

Setup

Starter Kit

git clone https://github.com/webiny/website-builder-nextjs.git my-website
cd my-website
npm install

Ensure @webiny/website-builder-nextjs and @webiny/sdk versions in package.json match your Webiny version (yarn webiny --version in your Webiny project).

Environment Variables

# .env
NEXT_PUBLIC_WEBSITE_BUILDER_API_KEY=your_wb_api_key
NEXT_PUBLIC_WEBSITE_BUILDER_API_HOST=https://your-cloudfront-url.cloudfront.net
NEXT_PUBLIC_WEBSITE_BUILDER_ADMIN_HOST=http://localhost:3001
NEXT_PUBLIC_WEBSITE_BUILDER_API_TENANT=root

Editor Components

An editor component has two parts:

  1. React component -- renders the UI, receives configured values via inputs prop
  2. Manifest -- metadata (name, label, group, inputs) that tells the editor about the component

Creating a Component

// src/editorComponents/Banner.tsx
import React from "react";
import { ComponentProps } from "@webiny/website-builder-nextjs";

interface BannerInputs {
  headline: string;
  ctaLabel: string;
  ctaUrl: string;
}

export function Banner({ inputs: { headline, ctaLabel, ctaUrl } }: ComponentProps<BannerInputs>) {
  return (
    <div className="bg-primary py-12 px-6 text-center text-white">
      <h2 className="text-3xl font-bold mb-4">{headline}</h2>
      {ctaLabel && ctaUrl && (
        <a
          href={ctaUrl}
          className="inline-block bg-white text-primary font-semibold px-6 py-3 rounded-md"
        >
          {ctaLabel}
        </a>
      )}
    </div>
  );
}

Registering Components

The editorComponents array must be in a "use client" file:

// src/editorComponents/index.tsx
"use client";
import { createComponent, createTextInput } from "@webiny/website-builder-nextjs";
import { Banner } from "./Banner";

export const editorComponents = [
  createComponent(Banner, {
    name: "Custom/Banner",
    label: "Banner",
    group: "custom",
    inputs: [
      createTextInput({
        name: "headline",
        label: "Headline",
        description: "The main headline text.",
        defaultValue: "Ready to get started?"
      }),
      createTextInput({
        name: "ctaLabel",
        label: "Button Label",
        defaultValue: "Get started"
      }),
      createTextInput({
        name: "ctaUrl",
        label: "Button URL",
        defaultValue: "/"
      })
    ]
  })
];

Important: The "use client" directive is required because component registration communicates with the editor via the browser. However, components imported here can still be Server Components if they don't have their own "use client" directive.

Component Name Convention

Use a namespaced string: "YourNamespace/ComponentName". Component names are stored in page documents -- treat them as stable identifiers; renaming breaks existing pages.

Input Types

Factory Function Use Case
createTextInput Single-line text, URLs, labels
createLongTextInput Multi-line text
createNumberInput Numeric values
createBooleanInput Toggle / checkbox
createColorInput Color picker
createDateInput Date / date-time picker
createSelectInput Dropdown with predefined options
createRadioInput Radio button group
createTagsInput List of tags
createObjectInput Nested object (group of sub-inputs)
createLexicalInput Rich text (Lexical editor)
createFileInput File / media picker
createSlotInput Slot for nesting other components

Each factory accepts: name, label, description, defaultValue, and type-specific options.

TypeScript prop types for each input

The input factories above define what the editor sidebar shows. Separately, each input produces a value at runtime that's passed into your React component via props.inputs. The runtime shape is not always a primitive — the most important case is file / image inputs, which are objects, not strings. Typing them as string compiles but breaks as soon as you try to read .src, .width, etc.

Use this table as the source of truth when you write the ComponentProps<T> generic for a component. These shapes come from the actual SDK usage (see e.g. @webiny/website-builder-nextjs/editorComponents/Image.d.ts in the project's node_modules for the canonical file-input shape).

Input factory Type of inputs.<name> in the component
createTextInput string
createLongTextInput string
createNumberInput number
createBooleanInput boolean
createColorInput string (CSS color value, e.g. "#4632f5" or "var(--wb-theme-color-primary)")
createDateInput string (ISO-8601, e.g. "2026-04-16T14:06:00.000Z")
createSelectInput string (the value of the chosen option)
createRadioInput string (the value of the chosen option)
createTagsInput string[]
createFileInput { id: string; name: string; size: number; mimeType: string; src: string; width: number; height: number }object, NOT a string. Use .src for the URL.
createLexicalInput { html?: string; state?: string } — render with <div dangerouslySetInnerHTML={{ __html: inputs.<name>.html ?? "" }} />
createObjectInput An object literal matching the shape of its nested fields (e.g. { street: string; city: string; zip: string })
createSlotInput React.ReactNode (rendered children). With list: true, the factory wraps inside the field value — see Grid pattern below.

List inputs (list: true)

When an input is declared with list: true (either directly, or via a factory like createTagsInput which does it internally), the type in the component becomes an array of the base type:

  • createFileInput({ list: true })Array<{ id; name; src; … }>
  • createObjectInput({ name: "rows", list: true, fields: [...] }) → an array of objects matching the fields shape.
  • createSlotInput({ name: "columns", list: true, ... }) → an array of { children: React.ReactNode } (see the Grid component in @webiny/website-builder-react for the reference pattern).

Worked example: image + rich-text component

import React from "react";
import type { ComponentProps } from "@webiny/website-builder-nextjs";

interface FeatureCardInputs {
  headline: string;
  body: { html?: string };
  image: {
    id: string;
    name: string;
    size: number;
    mimeType: string;
    src: string;
    width: number;
    height: number;
  };
  tags: string[];
}

export function FeatureCard({
  inputs: { headline, body, image, tags }
}: ComponentProps<FeatureCardInputs>) {
  return (
    <article>
      <h3>{headline}</h3>
      {image?.src && (
        <img src={image.src} width={image.width} height={image.height} alt={headline} />
      )}
      {body?.html && <div dangerouslySetInnerHTML={{ __html: body.html }} />}
      <ul>
        {tags.map(tag => (
          <li key={tag}>{tag}</li>
        ))}
      </ul>
    </article>
  );
}

Note how image is typed as the full object, not a plain string — otherwise the component would compile but blow up at runtime when it tried to read image.src.

Component Groups

Groups organize the editor's component palette:

// src/contentSdk/groups.ts
import { registerComponentGroup, type ComponentManifest } from "@webiny/website-builder-nextjs";

export const registerComponentGroups = () => {
  registerComponentGroup({
    name: "basic",
    label: "Basic",
    description: "Components for simple content creation"
  });
  registerComponentGroup({
    name: "custom",
    label: "Custom",
    description: "Assorted custom components",
    filter: (component: ComponentManifest) => !component.group
  });
};

The filter option creates a catch-all group for components without an explicit group.

Theming

The theme system has three files that work together:

1. theme.css -- CSS Custom Properties

/* src/theme/theme.css */
@import "@webiny/website-builder-nextjs/lexical.css";

:root {
  --wb-theme-color-primary: #4632f5;
  --wb-theme-color-secondary: #00ccb0;
  --wb-theme-color-background: #ffffff;
  --wb-theme-color-surface: #f9f9f9;
  --wb-theme-color-text-base: #0a0a0a;
  --wb-theme-color-text-muted: #6b7280;
  --wb-theme-color-border: #e5e7eb;
  --wb-theme-font-family: "Inter", sans-serif;
}

.wb-heading-1 {
  font-weight: 700;
  line-height: 1.2;
  font-size: clamp(2rem, 1.5rem + 1.5vw, 3rem);
}

.wb-paragraph-1 {
  font-weight: 400;
  line-height: 1.6;
  font-size: clamp(0.95rem, 0.9rem + 0.25vw, 1rem);
}

2. theme.ts -- Theme Registration

// src/theme/theme.ts
import { createTheme } from "@webiny/website-builder-nextjs";

declare const __THEME_CSS__: string;
export const css = __THEME_CSS__;

export const theme = createTheme({
  css,
  fonts: ["https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"],
  colors: [
    { id: "color-primary", label: "Primary", value: "var(--wb-theme-color-primary)" },
    { id: "color-secondary", label: "Secondary", value: "var(--wb-theme-color-secondary)" },
    { id: "color-background", label: "Background", value: "var(--wb-theme-color-background)" },
    { id: "color-text-base", label: "Text", value: "var(--wb-theme-color-text-base)" }
  ],
  typography: {
    headings: [{ id: "heading1", label: "Heading 1", tag: "h1", className: "wb-heading-1" }],
    paragraphs: [{ id: "paragraph1", label: "Paragraph 1", tag: "p", className: "wb-paragraph-1" }],
    quotes: [{ id: "quote", label: "Quote", tag: "blockquote", className: "wb-blockquote-1" }],
    lists: [{ id: "list1", label: "List 1", tag: "ul", className: "wb-unordered-list-1" }]
  }
});
  • colors populates the editor's color picker
  • typography populates the editor's typography toolbar
  • fonts injects fonts into the editor iframe

3. tailwind.css -- Tailwind Bridge

/* src/theme/tailwind.css */
@import "tailwindcss";

@theme inline {
  --font-sans: InterVariable, sans-serif;
  --color-primary: var(--wb-theme-color-primary);
  --color-secondary: var(--wb-theme-color-secondary);
  --color-text-base: var(--wb-theme-color-text-base);
}

This bridges WB CSS variables to Tailwind tokens, enabling bg-primary, text-primary, etc. in your components.

Changing Fonts (4 Files)

When switching fonts, update all four places:

File What to Update
src/app/layout.tsx Font import and config (e.g., import { Geist } from "next/font/google")
src/theme/tailwind.css --font-sans token
src/theme/theme.css --wb-theme-font-family variable
src/theme/theme.ts fonts array URL (must include same weight range as layout.tsx)

Server Components Fetching CMS Data

Build editor components that fetch data from the Headless CMS at render time:

// src/editorComponents/ProductListing.tsx
import React from "react";
import { ComponentProps } from "@webiny/website-builder-nextjs";
import { sdk } from "@/lib/webiny";
import type { Product } from "@/lib/types";
import type { CmsEntryData } from "@webiny/sdk";

interface ProductListingInputs {
  heading: string;
  limit: string;
}

export async function ProductListing({
  inputs: { heading, limit }
}: ComponentProps<ProductListingInputs>) {
  const parsedLimit = parseInt(limit, 10) || 6;

  const result = await sdk.cms.listEntries<Product>({
    modelId: "product",
    limit: parsedLimit,
    sort: ["values.name_ASC"]
  });

  if (!result.isOk()) {
    return <div className="text-red-600">Failed to load products: {result.error.message}</div>;
  }

  const products: CmsEntryData<Product>[] = result.value.data;

  return (
    <section className="py-12 px-6">
      {heading && <h2 className="text-3xl font-bold text-center mb-8">{heading}</h2>}
      <ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 max-w-5xl mx-auto">
        {products.map(product => (
          <li key={product.id} className="border rounded-lg p-6">
            <h3 className="text-xl font-semibold">{product.values.name}</h3>
            <p className="text-lg font-bold mt-2">${product.values.price.toFixed(2)}</p>
          </li>
        ))}
      </ul>
    </section>
  );
}

Register it (async Server Components work even though index.tsx is "use client"):

createComponent(ProductListing, {
  name: "Custom/ProductListing",
  label: "Product Listing",
  inputs: [
    createTextInput({
      name: "heading",
      label: "Section Heading",
      defaultValue: "Our Products"
    }),
    createTextInput({ name: "limit", label: "Number of products", defaultValue: "6" })
  ]
});

To use the Headless CMS SDK, initialize it in src/lib/webiny.ts with a Read API key (see the webiny-sdk skill).

Data Flow

Editor -> saves page document to Webiny API
           (document: component name + input values)

Next.js request/build
  -> contentSdk.getPage("/slug") -> returns page document
  -> DocumentRenderer matches component name to React component
  -> Component renders (Server Component may fetch CMS data)

Quick Reference

SDK package:      @webiny/website-builder-nextjs
Component type:   import { ComponentProps } from "@webiny/website-builder-nextjs";
Registration:     createComponent(ReactComponent, { name, label, inputs })
Input factories:  createTextInput, createNumberInput, createBooleanInput, etc.
Theme:            createTheme({ css, fonts, colors, typography })
Groups:           registerComponentGroup({ name, label, description })

Related Skills

  • webiny-sdk -- Using the Headless CMS SDK inside Website Builder components
  • webiny-project-structure -- Webiny project setup and extension registration
信息
Category 编程开发
Name webiny-website-builder
版本 v20260507
大小 18.25KB
更新时间 2026-05-08
语言