ForgeWP

Create Your First Gutenberg Block

Scaffold a ForgeWP block, edit its controls and frontend markup, then export it into WordPress.

A Gutenberg block is a content unit an editor can insert into a WordPress post or page, such as a callout, profile card, or promotional banner. A regular React component is part of your theme layout; a block is registered with WordPress so editors can add it in the block editor.

ForgeWP lets you author blocks in TSX. The compiler generates the WordPress block metadata and PHP renderer from a defineBlock() declaration.

To build a complete example alongside this guide, use Build an Editable Hero Block. Its linked practice exercise adds a CTA and image.

There is no single required way to make a component part of WordPress content. Choose the path that matches who owns the content:

Table · 2 columns
You need
Use
A block with explicit editor controls and a deliberate editor/frontend split
defineBlock()
An existing component exposed as a block with generated wrapper code
@forgewp-block
A layout block that contains other blocks
defineBlock() with innerBlocks, usually scaffolded by make:shell
A fixed React layout with an editable WordPress content slot
<BlockArea />
Fields shared between a page template, ACF, and a block
defineEditable() plus pickEditable() or mergeEditable()

These choices can be combined. For example, an annotated component can use a shared editable schema, and a parent shell can contain blocks that each expose their own controls.

Scaffold a block

From your theme root, run:

Terminal
pnpm forgewp make:block PromoCard --attributes=title,description

The React adapter creates src/blocks/PromoCard.tsx. The command makes string attributes named title and description, an editor form for changing them, and starter visitor-facing markup. If you omit --attributes, it uses title,description by default. If a file with that name already exists, the command stops rather than replacing it.

Attribute names must be unique JavaScript property identifiers, such as heading or cta_label; heading:string is not a typed-field syntax for this command. Change types and controls in the generated source. Use pnpm forgewp make:block --help or pnpm forgewp make:shell --help to see the authoring choices without generating anything.

The generated file is your source. Export creates block.json, render.php, and shared editor assets from it. To preview saved instances locally, import the definition into BlockArea.previewBlocks and put serialized block content in the current mock post. See local block previews.

Open the generated file and find the defineBlock() call. It describes the block name, the attributes WordPress stores, and two rendering functions:

  • edit is the block-editor view. It receives attributes and setAttributes so controls can show and update the saved values.
  • save can describe visitor-facing markup for a handcrafted block. ForgeWP compiles supported JSX into the block's PHP renderer; the property is optional because generated and dynamic block paths can supply the renderer another way.

Choose an authoring path

ForgeWP has two component-to-block authoring paths. They produce the same exported WordPress block format, but they serve different needs:

Table · 4 columns
Path
How it works
Best for
Main limitation
defineBlock()
Export a block definition from src/blocks/. You control the block metadata, attributes, edit, save, and innerBlocks.
Custom editor interfaces, explicit attributes, static or dynamic rendering, and parent blocks.
You write the block definition and connect the component yourself.
@forgewp-block
Add the annotation to an exported component or props interface anywhere under src/. ForgeWP discovers it and generates a defineBlock() wrapper in src/blocks/generated/.
Reusing an existing section component as an insertable block with minimal ceremony.
Attribute inference is intentionally limited, and the generated wrapper's editor behavior is less expressive than a handwritten definition.

pnpm forgewp make:block Name scaffolds the first path; it is a generator, not a third export mechanism. Register components in src/blocks/ with defineBlock() or the @forgewp-block annotation so the compiler can discover them.

Understand the two views

The same block has an editing experience in wp-admin and output on the visitor's page. ForgeWP builds different pieces for each context:

Flowchart
Rendering diagram...

WordPress stores a dynamic block's name and attributes in post content. When it displays the page, its block renderer calls the generated render.php; the block is not just a screenshot pasted into the editor.

Edit the block declaration

This smaller example shows the main shape of a block. The edit inputs update attributes; the save markup uses those same values for the generated frontend renderer.

src/blocks/PromoCard.tsx (simplified)
1import { defineBlock } from "@forgewp/react"
2
3export default defineBlock({
4  name: "promo-card",
5  title: "Promo Card",
6  category: "design",
7  icon: "megaphone",
8  attributes: {
9    title: { type: "string", default: "Launch faster" },
10    description: { type: "string", default: "Build with ForgeWP." },
11  },
12  edit: ({ attributes, setAttributes }) => (
13    <div>
14      <label>
15        Title
16        <input
17          value={attributes.title}
18          onChange={(event) =>
19            setAttributes({ title: event.currentTarget.value })
20          }
21        />
22      </label>
23      <label>
24        Description
25        <textarea
26          value={attributes.description}
27          onChange={(event) =>
28            setAttributes({ description: event.currentTarget.value })
29          }
30        />
31      </label>
32    </div>
33  ),
34  save: ({ attributes }) => (
35    <section className="border border-border bg-card p-6">
36      <h2 className="text-2xl font-bold">{attributes.title}</h2>
37      <p className="mt-2 text-muted-foreground">{attributes.description}</p>
38    </section>
39  ),
40})

The CLI-generated file includes more styling and input markup than this teaching example. You can keep that scaffold and edit it gradually. name is the block slug; ForgeWP adds the forgewp/ namespace when it writes the block metadata. The icon value is a WordPress Dashicon slug.

Preview an interactive block in the editor

Interactive visitor components may use React state, browser APIs, animations, or network requests that should not run in Gutenberg's canvas. By default, ForgeWP shows a small interactive-preview notice for these blocks. Add an optional editorPreview when editors need to see the section's real shape:

src/blocks/ContactForm.tsx (excerpt)
1export default defineBlock({
2  name: "contact-form",
3  title: "Contact Form",
4  attributes: {
5    heading: { type: "string", default: "Contact us" },
6  },
7  edit: () => null,
8  editorPreview: ({ attributes }) => (
9    <section>
10      <h2>{attributes.heading}</h2>
11      <label>
12        Email
13        <input type="email" disabled />
14      </label>
15      <button type="button" disabled>
16        Send
17      </button>
18    </section>
19  ),
20  save: () => <InteractiveContactForm />,
21})

editorPreview is used only in the Gutenberg canvas when ForgeWP detects that the visitor renderer is interactive. It does not replace save, change the generated visitor renderer, or load the preview as a visitor script. Keep it representative but inert: do not submit forms, start animations, or depend on live data. For forms, use disabled sample controls here and handle real submissions in the visitor component with submitWpForm() or useWpForm(). Without editorPreview, the existing notice remains the fallback.

Export and inspect the generated files

Run the normal theme export:

Terminal
pnpm export

For this block, the theme output includes files similar to:

Generated theme files
blocks/
└── promo-card/
    ├── block.json
    └── render.php
assets/
└── forgewp-editor.js

block.json describes the block to WordPress, including its name, title, category, attributes, and PHP render file. ForgeWP registers the block from functions.php. The editor bundle is shared by the theme's ForgeWP blocks; it is not a separate edit.js generated for every block.

After export, install the ZIP on a WordPress test site. Open a post or page in the block editor and search the inserter for the title declared in defineBlock(). The untouched CLI scaffold calls this block Sharp Promo Card; the simplified example above calls it Promo Card. Insert the block, change its fields, save the post, and view the page on the frontend.

Do I need to write `block.json` or `render.php` myself?

No. ForgeWP generates those files during export from the block declaration. Edit the TSX source in src/blocks/ and export again; do not make durable changes to generated output files.

When to use a block

  • Use a Gutenberg block when an editor should insert a reusable section into post or page content and configure its attributes.
  • Use a regular React component when the section belongs to a fixed theme template and should not be independently inserted from the block inserter.
  • Use a custom page template when the whole WordPress Page needs a distinct layout. See Create a Custom Page Template.

For adapting an existing React component into a block and making text editable directly in the canvas, continue to Turn a React Component into a Block.