9.6 KiB
Divi 5 + Claude Code: Lessons from First Build Session
Written 2026-05-07 by Stevenson, after building Kate Gathard's hero section as a working reproduction on demo-elysian.willcureton.com. For whoever (Hans, Penelope, the designer agent) inherits this work next.
1. The big realisation
Divi 5 is not Divi 4. It's a complete rewrite. Divi 4 shortcodes ([et_pb_section]...[et_pb_blurb...]) still load for backward compatibility, but Divi 5 silently drops most of their attributes when rendering. If you write Divi 4 shortcode attributes via the REST API, the body text usually shows up but colours, padding, alignment, animations, etc. don't. Don't use Divi 4 shortcodes. Write Divi 5 native blocks.
2. Divi 5 native block format
Pages are stored as post_content containing Gutenberg-style block comments:
<!-- wp:divi/section {ATTRS_JSON} -->
<!-- wp:divi/row {...} -->
<!-- wp:divi/column {...} -->
<!-- wp:divi/heading {...} /-->
<!-- wp:divi/text {...} /-->
<!-- /wp:divi/column -->
<!-- /wp:divi/row -->
<!-- /wp:divi/section -->
You write the same JSON the Visual Builder would write, programmatically. Update via WordPress REST API (POST /wp-json/wp/v2/pages/{id} with content body).
3. The per-breakpoint attribute wrapping
Every attribute value is wrapped three layers deep. Divi 5 stores per-breakpoint, per-state values everywhere:
{
"attributeName": {
"subPath": {
"desktop": {
"value": "..." // ← the actual value lives here
}
}
}
}
Common breakpoint keys: desktop, tablet, phone. Common state keys: value (default), hover, sticky.
Forgetting this wrapping is the #1 cause of WordPress 500 errors. The PHP renderer expects an array; if you pass a flat string, you get Argument #1 must be of type array, string given.
4. Module names — match the underlying directory
Module block names match the directory in /wp-content/themes/Divi/includes/builder-5/server/Packages/ModuleLibrary/. Hyphenated, not underscored:
| Right | Wrong |
|---|---|
divi/contact-form |
divi/contact_form |
divi/contact-field |
divi/contact_field |
divi/signup |
(different module — for Mailchimp/AC integration) |
divi/heading |
(use this for h1/h2/h3, not divi/text with a <h1> inside) |
divi/text |
(paragraph blocks; uses content.innerContent, not title.innerContent) |
Don't guess module names — list the directory first.
5. Common attribute paths that work
Verified in this build:
| What you want | Attribute path |
|---|---|
| Heading text content | title.innerContent.desktop.value |
| Heading level (h1/h2/etc.) | level.desktop.value |
| Heading colour | title.decoration.font.font.desktop.value.color |
| Heading font family | title.decoration.font.font.desktop.value.family |
| Heading font size | title.decoration.font.font.desktop.value.size |
| Heading font weight | title.decoration.font.font.desktop.value.weight (use string "300", "400", "700") |
| Heading text-align | title.decoration.font.font.desktop.value.textAlign |
| Text body content | content.innerContent.desktop.value (HTML string with <p> tags) |
| Text body alignment | module.advanced.text.text.desktop.value.orientation (NOT module.decoration.text.text) |
| Text body font family | module.decoration.bodyFont.body.font.desktop.value.family |
| Text body colour | module.decoration.bodyFont.body.font.desktop.value.color |
| Section background video | module.decoration.background.desktop.value.video.mp4 |
| Section background image | module.decoration.background.desktop.value.image.url |
| Module width | module.advanced.sizing.width.desktop.value |
| Module max-width | module.advanced.sizing.maxWidth.desktop.value |
| Module padding | module.decoration.spacing.padding.desktop.value.{top,bottom,left,right} |
| Module border radius | module.decoration.border.desktop.value.radius.{topLeft,topRight,bottomLeft,bottomRight,sync} |
| Module border style | module.decoration.border.desktop.value.styles.all.{width,color,style} |
| Module animation | module.decoration.animation.desktop.value.{style,direction,duration,delay,intensity,startingOpacity,speedCurve} |
| Free-form CSS | css.desktop.value.freeForm (a string of raw CSS) |
Decoration is a sibling of innerContent under each named element, not nested inside.
6. Free-Form CSS — the canonical escape hatch
When an attribute path doesn't exist (or you can't find it), use Free-Form CSS via css.desktop.value.freeForm. The string takes raw CSS. The keyword selector is replaced by the rendered module's specific class at runtime. Examples:
selector { max-width: 400px; margin: 0 auto; }
selector::before { content:""; position:absolute; inset:0; background:rgba(0,0,0,0.3); }
selector .et_pb_button:hover { background-color: rgba(255,255,255,0.35) !important; }
The official Elegant Themes pattern for an overlay over a video background is:
selector::before { content:""; display:block; position:absolute; top:0; left:0; width:100%; height:100%; background-color:rgba(0,0,0,0.5); z-index:1; }
selector .et_pb_row { z-index:2; }
Free-Form CSS is what makes Divi 5 fully programmable. Most "I can't find the attribute path" problems collapse into this.
7. CSS — render the page first, find the real class names
Divi 5's rendered CSS classes are not always what you'd guess from the block attribute paths. Always check the actual HTML before writing CSS.
Examples from this build:
| Looks like | Actual class |
|---|---|
| Field input | .input (not .et_pb_contact_field — that's the wrapper div) |
| Button wrapper | .et_pb_button_wrapper |
| Contact form bottom container | .et_contact_bottom_container |
| Section overlay target | .et_pb_row (z-index:2 lifts content above the ::before overlay) |
Workflow: curl the rendered page, find the element, read its actual classes, write CSS targeting those.
8. Hover state on buttons
Divi adds an arrow on button hover via ::after { content: "5"; opacity: 0 → 1; padding-shift }. To replace with a clean overlay-on-hover effect:
selector .et_pb_button::after,
selector .et_pb_button:hover::after,
selector .et_pb_button:focus::after {
display:none !important; content:none !important; opacity:0 !important;
visibility:hidden !important; width:0 !important; margin:0 !important; padding:0 !important;
}
selector .et_pb_button:hover {
background-color: rgba(255,255,255,0.35) !important;
padding-left:20px !important; padding-right:20px !important; /* prevent shift */
}
The padding-left/right override on :hover is needed because the default Divi hover shifts padding to make space for the (now-hidden) arrow.
9. CSS specificity warnings
Divi inlines a lot of styles. To win specificity battles, prefix with body or use !important. From CJ Simon's Divi5-ToolKit:
body .et_pb_button { background-color:#000 !important; } /* Standard override */
body .et_pb_button.my-btn { ... } /* With custom class */
10. Workflow: pair-programming
What worked best: human in the Visual Builder UI, AI driving block JSON via REST API. Human directs verbally ("centre that, make it bigger, change the colour"), AI translates to attribute paths, edits, returns. Refresh, iterate.
When the AI gets stuck on an attribute path, the human can do it in the UI to expose the saved JSON, then the AI reads from there to learn the pattern. This is how I learned the divi/contact-form block structure — Will did it in the UI first, I read the JSON.
11. Tools to layer on Claude Code
- CJ Simon's Divi5-ToolKit (free, MIT-ish on GitHub): comprehensive Divi 5 selector reference + CSS patterns + module knowledge. Cloned to
/opt/wordpress-demo/plugins/Divi5-ToolKit/. Registered globally in/root/.claude/settings.json. Slash commands available next session boot. - Respira (€95/year, paid plugin live on demo-elysian): MCP server with 172 tools, duplicate-before-edit safety, builder-aware module operations across 11 builders. We installed it but mostly used the underlying REST API directly during this session.
- wordpress-rest-api skill at
/mnt/skills/user/wordpress-rest-api/: generic CRUD on posts/pages/products via WordPress REST API + Application Password.
12. What I haven't yet figured out
- The full attribute schema for every module — only verified the ones used in this build. The PHP
*PresetAttrsMap.phpfiles are partial (preset-only); the canonical full schema lives in the React/JS Visual Builder bundle. - The Divi 5 design preset / variable system — touched briefly via attribute paths but didn't drive any preset linkage.
- Composable Settings (Divi 5.2+) — toggle-any-design-option-on-any-sub-element. Probably the cleanest path for fine control once the core schema is mapped.
13. The empirical loop
For any attribute you don't know:
- Do it in the Visual Builder UI yourself
- Read the saved post_content via
GET /wp-json/wp/v2/pages/{id}?context=edit - Note the JSON structure
- Reproduce that structure in subsequent builds
Faster than reading source code in most cases. The Divi developer docs are reliable for capability surface but rarely show the exact attribute names.
Closing
The architecture works. The pair-programming pattern works. Free-Form CSS is the universal escape hatch. The 101 modules will mostly behave the same way once you internalise the per-breakpoint wrapping.
Don't fight the schema — read what the Visual Builder writes and copy that. Use Divi5-ToolKit for selector reference. Use Respira when safe-edit matters. Build empirically and iterate.
— Stevenson