ForgeWP

Solution: Hero CTA and Image

A complete implementation of the hero exercise, with saved text, URL and image controls, and an explanation of the bindings.

This is one implementation of Add an Editable CTA and Image. Try the exercise first if you want to build it independently.

Complete block

Replace src/blocks/HeroBlock.tsx with the following file. It keeps the original block name and attributes, and adds fields for the CTA and image. Use this as a replacement for the recipe block, rather than keeping both definitions in the same project.

src/blocks/HeroBlock.tsx
1import { defineBlock, WpEditable, WpImage } from "@forgewp/react"
2
3export default defineBlock({
4  name: "hero-block",
5  title: "Editable Hero",
6  category: "design",
7  icon: "cover-image",
8  attributes: {
9    heading: {
10      type: "string",
11      label: "Heading",
12      default: "A quieter place to stay",
13    },
14    description: {
15      type: "string",
16      label: "Description",
17      default: "Comfortable rooms, thoughtful details, and time to unwind.",
18    },
19    ctaLabel: {
20      type: "string",
21      label: "CTA label",
22      default: "Explore our rooms",
23    },
24    ctaUrl: {
25      type: "string",
26      control: "url",
27      label: "CTA destination",
28      default: "#rooms",
29    },
30    cover: {
31      type: "object",
32      control: "image",
33      label: "Hero image",
34      default: null,
35    },
36    imageAlt: {
37      type: "string",
38      label: "Image alternative text",
39      default: "",
40    },
41  },
42  edit: ({ attributes, setAttributes }) => (
43    <section className="bg-slate-950 px-6 py-16 text-white md:px-12 md:py-24">
44      <div className="mx-auto grid max-w-6xl items-center gap-10 md:grid-cols-2">
45        <div>
46          <WpEditable
47            tagName="h2"
48            value={attributes.heading}
49            onChange={(value) => setAttributes({ heading: value })}
50            className="text-4xl font-bold tracking-tight md:text-6xl"
51          />
52          <WpEditable
53            tagName="p"
54            value={attributes.description}
55            onChange={(value) => setAttributes({ description: value })}
56            className="mt-6 text-lg leading-relaxed text-slate-300"
57          />
58          <a
59            href={attributes.ctaUrl}
60            className="mt-8 inline-flex bg-white px-6 py-3 font-semibold text-slate-950"
61          >
62            <WpEditable
63              tagName="span"
64              value={attributes.ctaLabel}
65              onChange={(value) => setAttributes({ ctaLabel: value })}
66            />
67          </a>
68        </div>
69        {attributes.cover && (
70          <WpImage
71            src={attributes.cover}
72            alt={attributes.imageAlt}
73            className="aspect-[4/3] w-full object-cover"
74          />
75        )}
76      </div>
77    </section>
78  ),
79  save: ({ attributes }) => (
80    <section className="bg-slate-950 px-6 py-16 text-white md:px-12 md:py-24">
81      <div className="mx-auto grid max-w-6xl items-center gap-10 md:grid-cols-2">
82        <div>
83          <WpEditable
84            tagName="h2"
85            value={attributes.heading}
86            className="text-4xl font-bold tracking-tight md:text-6xl"
87          />
88          <WpEditable
89            tagName="p"
90            value={attributes.description}
91            className="mt-6 text-lg leading-relaxed text-slate-300"
92          />
93          <a
94            href={attributes.ctaUrl}
95            className="mt-8 inline-flex bg-white px-6 py-3 font-semibold text-slate-950"
96          >
97            <WpEditable tagName="span" value={attributes.ctaLabel} />
98          </a>
99        </div>
100        {attributes.cover && (
101          <WpImage
102            src={attributes.cover}
103            alt={attributes.imageAlt}
104            className="aspect-[4/3] w-full object-cover"
105          />
106        )}
107      </div>
108    </section>
109  ),
110})

Why these choices work

Table · 2 columns
Choice
Purpose
Keep hero-block, heading, and description
Preserve the existing block's connection to its saved text
Bind ctaLabel through WpEditable
Edit the visible label directly in the canvas
Declare ctaUrl with control: "url"
Expose an appropriate destination control
Bind the anchor's href to ctaUrl
Use the saved destination in editor and visitor markup
Declare cover with control: "image"
Give editors a Media Library picker
Pass cover to WpImage
Render the selected image through ForgeWP's image primitive
Expose imageAlt separately
Let editors describe the image in the context of this hero
Use the same static Tailwind classes in both views
Keep the editor and visitor layouts aligned

Gutenberg's generated canvas intercepts links so editing a label or destination stays within wp-admin. Test the saved destination on the visitor page. The initial #rooms link needs a matching target on the Page; replace it with a real destination during verification.

The image is optional. Before selection, the text occupies the first grid column on wider screens; the second column remains empty. You can refine that layout in a later iteration. On narrow screens, the grid stacks its content.

Verify in WordPress

Theme terminal: check and export the solution
pnpm typecheck
pnpm forgewp export --validate --strict
  1. Update the test site's theme with this export.
  2. Open the Page containing your existing hero and confirm its heading and description.
  3. Select the block and choose an image using the Hero image control. Depending on the editor layout, open the block settings sidebar to see its controls.
  4. Enter Image alternative text for an informative image, and choose a working CTA destination.
  5. Edit the label in the canvas, save, reload the editor, and inspect the visitor page.
  6. Duplicate the block and give the copy different text and an image. Confirm both instances retain their own values.
  7. Check a narrow viewport, a wider viewport, and a block with no cover selected.

For generated-file inspection, check the attributes in block.json, their PHP bindings in render.php, and the inline controls in the shared editor script. These checks complement the editor and visitor checks above.

Continue learning

Review Choose How Block Content Is Edited for other control types, or return to Build an Editable Hero Block to compare the original two-field version.