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.
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
Choice Purpose Keep hero-block, heading, and descriptionPreserve the existing block's connection to its saved text Bind ctaLabel through WpEditableEdit the visible label directly in the canvas Declare ctaUrl with control: "url"Expose an appropriate destination control Bind the anchor's href to ctaUrlUse the saved destination in editor and visitor markup Declare cover with control: "image"Give editors a Media Library picker Pass cover to WpImageRender the selected image through ForgeWP's image primitive Expose imageAlt separatelyLet 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
pnpm typecheck
pnpm forgewp export --validate --strict- Update the test site's theme with this export.
- Open the Page containing your existing hero and confirm its heading and description.
- 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.
- Enter Image alternative text for an informative image, and choose a working CTA destination.
- Edit the label in the canvas, save, reload the editor, and inspect the visitor page.
- Duplicate the block and give the copy different text and an image. Confirm both instances retain their own values.
- 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.