---
name: divi-build-page
description: Build or rebuild a page in the Divi 5 builder end-to-end — from scratch or from a source such as Gutenberg, HTML, screenshots, Figma, or another builder. Activate when the user asks to create a new Divi page, recreate an existing page in Divi, style Divi modules or pages, or work with Divi modules, loops, or dynamic content. Covers the Divi 5 block-module model, the hard rules never to fake layouts with raw HTML and never to style with custom CSS, the styling hierarchy that replaces custom CSS, and how loops and dynamic content work.
---

# Building a page in Divi 5

This is the Divi 5 playbook. Use it when the target is Divi, whether building
from scratch or reconstructing from another source. It targets **Divi 5** (the
WordPress-block module model), not Divi 4 shortcodes.

Do everything through the dedicated `novamira/divi-*` abilities — there is one
for every Divi operation (content, modules, dynamic content, loops, presets,
colors, variables, fonts, display conditions, library, theme builder, and
schema discovery). Supporting abilities such as `novamira/create-post` and
`novamira/memory-*` are allowed when they support the workflow.

**Never build or style a Divi page with `novamira/execute-php`,
`novamira/write-file`, or raw `post_content`.** There is no Divi gap that
requires PHP: writing the layout or its metadata by hand skips the validation
these abilities do and leaves the page uneditable in the Visual Builder (the
write abilities also refuse builder content pushed through `create-post` /
`update-post`). If you think an operation has no ability, call
`novamira/discover-abilities` and re-read this skill before reaching for PHP.

## Hard rules

These are not optional. They are the difference between a real Divi page and a
broken one.

1. **Never fake a layout with raw HTML. Build real Divi modules.**
   Do NOT drop a `divi/code` or `divi/fullwidth-code` module containing a blob
   of HTML to reproduce a design. That bypasses Divi's styling, responsiveness,
   dynamic content, and editability — the page looks right once and is dead in
   the editor. Compose the design from native modules (`divi/heading`,
   `divi/text`, `divi/image`, `divi/button`, `divi/blurb`, …) inside the
   structural skeleton. The Code module is only for a genuine code snippet the
   user explicitly asked to embed. `divi-list-modules` flags the code modules
   `raw_html: true` as a reminder, and the write abilities reject code modules
   unless the explicit `allow_code_modules: true` opt-in is passed — only set it
   when the user has explicitly confirmed they want a raw code module.

2. **Never style with custom CSS. Style with module attrs.**
   Do NOT write a stylesheet — not in a `divi/code` module, not in an inline
   `<style>` block, not via `execute-php`/`write-file`, and not by dumping rules
   into `css.freeForm`. CSS written that way is invisible and uneditable in the
   Divi builder — one missed bracket breaks the whole page, and it is exactly
   what the customer does not want. Every visual property (background, color,
   spacing, typography, border, shadow, sizing) has a native attr under
   `module.decoration.*` — discover it with `divi-get-style-schema` **before**
   concluding it doesn't exist. If a rule applies to more than one module, it's
   a global preset, color, or variable — not a stylesheet. See *Styling
   hierarchy* below.

3. **Respect the structural skeleton.** Every Divi page is
   `divi/section` › `divi/row` › `divi/column` › leaf modules. Leaf modules live
   inside columns, never loose at the top level. Rows define the column
   structure; put content modules in columns.

4. **Use loops for repeated, query-driven content** (see Loops) and **dynamic
   content for values bound to the post/site/query** (see Dynamic content) —
   never hardcode what should be dynamic.

## Domain model

Divi 5 stores a page as WordPress blocks in `post_content`
(`<!-- wp:divi/section {…attrs…} -->…`), round-tripped with the core block
parser. You work with a tree of module nodes.

- **Module names** are `divi/<slug>` (e.g. `divi/text`, `divi/blog`).
- **Structure**: `divi/section` › `divi/row` › `divi/column` › modules. Inner
  variants `divi/row-inner` / `divi/column-inner` nest a row inside a column.
- **Attributes** are nested with responsive wrappers. Most settable values sit
  at `<group>.<...>.<breakpoint>.value`, e.g. a text body is
  `content.innerContent.desktop.value`. Universal styles live under
  `module.decoration.<group>` (background, spacing, border, font, sizing, …).
  A path like `module.decoration.spacing.tablet.value` is **notation for nested
  objects** — pass it as `{"module":{"decoration":{"spacing":{"tablet":{"value":
  …}}}}}`, not as a single flat `"module.decoration.spacing.tablet.value"` key.
  Divi 5 only renders the nested shape; the abilities auto-nest a flat dotted key
  for you and warn, but write it nested from the start so a get round-trips
  cleanly.
- **Presets**: `attrs.modulePreset` stacks global presets onto a module.

## Discovery workflow (available now)

1. `novamira/divi-check-setup` — confirm Divi 5 is active (`d5_enabled`,
   `module_api_available`), that `page` is a builder post type, and the active
   `breakpoints` (the valid `<breakpoint>` keys for responsive values, e.g.
   `desktop`, `tablet`, `phone`).
2. `novamira/divi-list-modules` — browse the ~100 module types. Filter by
   `category` (`structure`, `module`, `child-module`, `fullwidth-module`) or
   `search` to keep the response small. `category` tells you the role; `raw_html`
   marks the code modules; `children` lists the named child modules a container
   hosts.
3. `novamira/divi-get-module-schema module=<name>` — for a chosen module, read
   its content/config `fields` (each with its `attrName` path and whether it
   accepts dynamic content) and its `style_groups`.
3b. `novamira/divi-get-style-schema module=<name>` — the styling counterpart:
   the universal **decoration** groups (background, spacing, border, fonts, …)
   plus the **advanced** module options that are otherwise hard to find — the CSS
   ID/class (group `htmlAttributes`), link, column gutter, text-shadow. Each row
   is tagged `segment` (`decoration` | `advanced`). Call without `group` for the
   summary, then `group=<name>` for its fields. Set a value via `divi-edit-module`
   at `<attrName>.<breakpoint>.value.<field>` — e.g.
   `module.decoration.spacing.desktop.value.padding.top`, or
   `module.advanced.htmlAttributes.desktop.value.id` to give a section a CSS ID
   for a nav anchor or custom CSS hook.
   **Style paths are per module — never reuse one module's path on another.**
   Text styling is the classic trap: a `divi/heading` aligns and colors its
   title at `title.decoration.font.font.desktop.value` (`textAlign`, `color`,
   `size`, …), but a `divi/text` module's body lives at
   `content.decoration.bodyFont.body.font.desktop.value` — writing the heading
   path on a text module is silently ignored. Read the target module's own
   `divi-get-style-schema` before styling it.
4. `novamira/divi-get-content post=<id>` — read an existing page as a flat,
   addressable node list. Each node has an `address` path (root sections `0`,
   `1`, …; children append `/<index>`), `parent`, `children`, and flags
   (`loop_enabled`, `dynamic_bindings`, `raw_html`). Pass `include_attrs=true`
   for the full per-node attributes.

## Build workflow (writes)

Build incrementally and re-read after structural changes (addresses shift).

- **From scratch, whole page**: assemble the tree and call
  `novamira/divi-set-content post=<id> content=[…]`. `content` is a nested list
  of `{name, attrs?, children?}`. Shape it as
  `section › row › column › leaf modules`. Set text via attrs (e.g.
  `content.innerContent.desktop.value`), never via a code module.
- **Incremental**: `novamira/divi-add-module` (insert under a `parent` address at
  an optional `position`; omit `parent` to add a top-level section),
  `novamira/divi-edit-module` (deep-merge partial `attrs` into the module at an
  `address`), `novamira/divi-delete-module` (remove a subtree),
  `novamira/divi-move-module` (reparent/reorder).
- **Addresses shift** after add/delete/move. Re-run `novamira/divi-get-content`
  before the next address-based edit rather than guessing the new path.
- Nesting and the raw-HTML rules are enforced on every write: an invalid
  parent, a leaf at the root, an unknown module, any code module without the
  `allow_code_modules` opt-in, or a code-only page is rejected with an
  actionable error.

Worked example — a hero section:
1. `divi-add-module module=section` → `0`
2. `divi-add-module parent=0 module=row` → `0/0`
3. `divi-add-module parent=0/0 module=column` → `0/0/0`
4. `divi-add-module parent=0/0/0 module=heading attrs={"content":{"innerContent":{"desktop":{"value":"Welcome"}}}}`
5. `divi-add-module parent=0/0/0 module=button`

(Or do all of it in one `divi-set-content` call — fewer round-trips.)

**Fix forward, never rebuild.** When the page doesn't match the reference, fix
the specific failing modules with targeted `divi-edit-module` calls at their
addresses — do not re-run `divi-set-content` on the whole page. Re-read with
`divi-get-content` first, verify one section at a time against the reference.
If two targeted attempts don't fix a module, stop and re-read its
`divi-get-module-schema` / `divi-get-style-schema` — the attr path is almost
certainly wrong; retrying the same write louder wastes the user's tokens.

## Styling hierarchy — use in this order, do not skip

The most common agent failure on Divi is reaching for CSS when a native attr
already covers the intent. **Before writing any CSS, call
`divi-get-style-schema` on the target module** and look for the property among
the decoration groups. If an attr exists, using CSS instead is wrong.

1. **Module decoration/advanced attrs** — the native mechanism, not a
   workaround. `module.decoration.<group>.<breakpoint>.value.*` for background,
   spacing, fonts, border, boxShadow, sizing; content-part styling lives on the
   module's own paths (`title.decoration.*`, `content.decoration.bodyFont.*` —
   read the module's schema). This is where ~95% of styling belongs.
2. **Global presets** for styling repeated across modules of the same type
   (buttons, cards, headings) — style once, apply everywhere.
3. **Global colors, design variables, global fonts** for brand tokens
   referenced by many modules.
4. **`css.freeForm`** — true escape hatch, ONLY for what attrs cannot express
   (pseudo-elements, keyframes, child selectors). Set it with
   `divi-edit-module` at `css.<breakpoint>.value.freeForm`; inside the rules,
   the literal word `selector` is replaced with the module's own selector, so
   the CSS stays scoped to the module. Writing `background`, `padding`,
   `font-size` or any other attr-covered property here is wrong — the write
   succeeds but comes back with a warning for a reason.
5. **Never** a `divi/code` module containing `<style>`. There is no tier below
   this — it is not styling, it is a bug you are introducing.

Anti-patterns (all observed in the field, all wrong):
- Writing a page-wide stylesheet into a `divi/code` module "to save N edits"
- Inline `<style>` blocks targeting module CSS IDs
- `css.freeForm` for properties `divi-get-style-schema` exposes
- Styling via `execute-php` / `write-file` instead of module attrs

## Columns and column widths

A row lays its columns out side by side automatically. Build a row with N
columns (2–6) and they render as an even N-up grid — equal widths are filled in
for you, so `section › row › [column, column, column]` is a three-up row with no
extra attributes. A single-column row stays full width.

For an uneven layout, set each column's width fraction at
`module.advanced.type.desktop.value` — a Divi fraction: `1_2`, `1_3`, `2_3`,
`1_4`, `3_4`, `1_5`, `2_5`, `3_5`, `4_5`, `1_6`, `5_6` (the fractions in one row
should add up to a full width, e.g. `2_3` + `1_3`). You only set the per-column
`type`; the row's overall `columnStructure` is derived from the columns for you.
Adding or removing a column in an even row re-distributes the widths
automatically; after changing the column count of a *custom* layout, re-set the
fractions you want.

## Loops

A query loop makes any loop-capable module repeat once per query result. Use the
abilities rather than hand-writing the attrs:

- `novamira/divi-list-loop-query-types` — valid `query_type` values, what each
  one's `sub_types` mean, and `order_by`/`order` options.
- `novamira/divi-enable-loop` — turn a loop on at `address` with `query_type`
  (default `post_types`), `sub_types` (e.g. `["post"]`), `order_by`, `order`,
  `per_page`, `offset`. Only modules flagged `loop_capable` by
  `divi-list-modules` accept one — note **`divi/blog` is not loop_capable**: it
  runs its own internal posts query, the generic loop repeats other modules.
- `novamira/divi-edit-loop` — change query params on a live loop (partial).
- `novamira/divi-disable-loop` — switch it off, keeping the config.

Worked example — a grid of the 6 latest posts:
1. Build `section › row › column › blurb` (the blurb is the module that repeats).
2. `divi-enable-loop address=0/0/0/0 query_type=post_types sub_types=["post"] order_by=date order=DESC per_page=6`.
3. Bind the blurb's title and image to the current post (next).

Inside a loop, bind the repeating module's fields to the current item with the
`loop_*` dynamic sources below.

### Per-item binding in loops

To make a looped child render the current item, bind its field to a `loop_*` dynamic source with `novamira/divi-apply-dynamic-content`: pass the field's `attrName` (from `novamira/divi-get-module-schema`, e.g. `content.innerContent`, `title.innerContent`) as `field`, and a `loop_*` id as `source`. Which `loop_*` ids resolve depends on the loop's `query_type`:

- **post_types** → post sources: `loop_post_title`, `loop_post_excerpt`, `loop_post_date`, `loop_post_featured_image`, `loop_post_author`, `loop_post_link`, ….
- **post_taxonomies / terms** (a `post_taxonomies` loop iterates terms) → term sources: `loop_term_name`, `loop_term_description`, `loop_term_featured_image`, `loop_term_permalink`, `loop_term_count`, ….
- **users / user_roles** → user sources: `loop_user_name`, `loop_user_email`, `loop_user_avatar`, `loop_user_username`, `loop_user_url`, ….
- **menus** → menu sources: `loop_menu_text`, `loop_menu_link`, `loop_menu_menu_order`, `loop_menu_attr_title`, `loop_menu_description`, ….

To bind per-item custom fields, use a `loop_*` meta source with `settings.meta_key`. The categorized reference per query type is `novamira/divi-list-loop-query-types`.

`divi-get-content` reports `loop_enabled: true` on a module whose loop is on.
(Under the hood the loop lives at `module.advanced.loop.desktop.value` with
`enable:"on"` and `subTypes` as `[{"value":"post"}, …]`.)

## Dynamic content

Bind a module field to a dynamic source so it resolves at render time instead of
hardcoding the value. Don't hand-encode it — use the abilities:

- `novamira/divi-list-dynamic-sources` — find the source id (compact list; pass
  `source=<id>` for its settings fields, `post=<id>` to surface ACF/WooCommerce
  sources). Common ids: `post_title`, `post_excerpt`, `post_featured_image`,
  `post_meta_key` (+ `settings.meta_key`), `site_title`, `product_*`, `loop_*`.
- `novamira/divi-apply-dynamic-content` — bind `field` (a `dynamic:true` attrName
  from `get-module-schema`, e.g. `content.innerContent`, `title.innerContent`) on
  the module at `address` to a `source`, with optional `settings`
  (e.g. `{"before":"$","after":""}`).
- `novamira/divi-clear-dynamic-content` — revert a field to a static value.

Under the hood the binding is stored as a render-resolvable inline token
`$variable({"type":"content","value":{"name":<source>,"settings":{…}}})$` at
`<field>.<breakpoint>.value` — a bare JSON object would render literally and
never resolve. `divi-get-content` reports `dynamic_bindings` per node.

## Global presets (the reusable styling layer)

Global module presets are reusable, named styling bundles for a module type —
Divi's nearest thing to a design system. Author a preset once and apply it to
many modules, so a restyle touches one place. **Prefer presets over repeating
the same raw `divi-edit-module` styling on every module.**

Discover and apply:
- `novamira/divi-list-global-presets` — discover presets (module, id, name,
  is_default); filter by `module`. An empty list means none exist yet.
- `novamira/divi-get-global-preset` — inspect one preset's styling attrs.
- `novamira/divi-apply-global-preset` — attach a preset to the module at
  `address` (sets `attrs.modulePreset`). The preset must be for that module's
  type — a `divi/text` preset only applies to a `divi/text` module.

Author and maintain:
- `novamira/divi-create-global-preset` — create a preset for a `module` with
  `attrs` and an optional `name`; returns its `preset_id`. **`attrs` uses the
  exact same shape as `divi-edit-module`** — the module's attributes object,
  e.g. `{"module":{"decoration":{"spacing":{"desktop":{"value":{"padding":{"top":"40px"}}}}}}}`.
  Discover attr paths with `divi-get-style-schema`.
- `novamira/divi-edit-global-preset` — rename (`name`) and/or restyle (`attrs`,
  deep-merged into the preset). Editing a preset restyles every module it is
  applied to at once.
- `novamira/divi-delete-global-preset` — remove a preset; modules keep their own
  attrs and simply stop inheriting it.
- `novamira/divi-set-default-preset` — make a preset the default for its module
  type, so every newly inserted module of that type starts on-brand (it does not
  restyle modules that already exist).

A good build flow: create presets for the components you repeat (buttons,
headings, cards), set sensible defaults, then build the page — applying presets
instead of hand-styling each module.

## Global colors (the brand palette)

Global colors are named, reusable colors — define the brand palette once and
bind module color fields to it, so a palette change restyles the whole site.
**Prefer global colors over hardcoding the same hex on many modules.**

- `novamira/divi-list-global-colors` — list the palette: each row has an `id`
  (`gcid-…`), `color`, `status`, `named` (true for the five built-in theme
  colors — primary, secondary, heading, body, link), and `var` (the CSS value
  to bind with).
- `novamira/divi-create-global-color` — add a custom color (`color` = any CSS
  color); returns its `id` and `var(--gcid-…)`.
- `novamira/divi-edit-global-color` — change a color's value and/or status by
  `id`. Works for custom colors and the five built-in theme colors (editing a
  named color updates it site-wide).
- `novamira/divi-delete-global-color` — remove a custom color (the five built-in
  theme colors can't be deleted; edit their value instead).

Bind a module to a global color by setting any color field to the `var` value
with `novamira/divi-edit-module` — e.g. a section background at
`module.decoration.background.desktop.value.color`, or a text/heading color.
Divi emits the color as a CSS custom property, so every bound element updates
when the global color changes.

## Design variables (tokens)

Design variables are named, reusable values — spacing, sizes, radii, strings,
image URLs, links — referenced as `var(--gvid-…)`. Define a token once and a
change propagates everywhere it is used. **Prefer tokens over repeating literal
values across modules.**

- `novamira/divi-list-variables` — list tokens: each row has `id` (`gvid-…`),
  `type` (numbers, strings, images, links, fonts), `label`, `value`, `var`, and
  `builtin` (true for the theme's font variables, which are read-only here).
- `novamira/divi-create-variable` — create a token of `type` (numbers, strings,
  images, links) with a `label` and `value`; returns its `id` and
  `var(--gvid-…)`. Use numbers for sizes/spacing/radii (e.g. `"16px"`,
  `"clamp(1rem,2vw,3rem)"`).
- `novamira/divi-edit-variable` — change a token's `label`, `value`, and/or
  `order` by `id`.
- `novamira/divi-delete-variable` — remove a token.

Reference a token by setting a module value to its `var` value with
`novamira/divi-edit-module` — e.g.
`module.decoration.spacing.desktop.value.padding.top` = `var(--gvid-…)`. Divi
emits the token as a `:root` custom property, so everything referencing it
updates when the token changes. For brand colors use the global-color
abilities; font variables come from the theme typography.

## Global fonts

Set the site-wide typography once instead of styling every module:

- `novamira/divi-get-global-fonts` — read the global fonts (an empty value means
  the Divi default is in effect).
- `novamira/divi-set-global-fonts` — set any of `heading_font`, `body_font`
  (family names), `heading_font_weight`, `body_font_weight`, `body_font_size`,
  `body_font_height`, and the tablet/phone base sizes. Only the fields you pass
  change; it applies site-wide on the front end. For per-module fonts, use
  `divi-edit-module`.

## Display conditions (conditional visibility)

Show or hide a module by visitor or context — login status, role, post/term,
date & time, device/browser, cookie, or WooCommerce state:

- `novamira/divi-list-condition-types` — the condition types (`conditionName`),
  each with its `display_rules` (the `displayRule` values) and the extra
  `settings` keys it reads.
- `novamira/divi-set-display-conditions` — set the conditions on the module at
  `address`. `conditions` is a list of `{conditionName, settings, operator?}`:
  `settings` carries the type's `displayRule` plus its keys; `operator` (`OR`
  default, or `AND`) joins to the next condition. The list replaces the module's
  current conditions. Example —
  `[{"conditionName":"loggedInStatus","settings":{"displayRule":"loggedIn"}}]`
  shows the module only to logged-in visitors.

## Interactions (motion)

Add motion to a module — a trigger on the module runs an effect on the module
itself. Most motion is already available as decoration attrs
(`module.decoration.animation` / `scroll` / `sticky` / `transform` /
`transition` / `filters` — discover them with `divi-get-style-schema` and set
via `divi-edit-module`). The trigger→effect **interactions** envelope has its
own two abilities:

- `novamira/divi-list-interaction-types` — the `triggers` (onClick, onHover,
  onScroll, onLoad, onMouseMove, onView, onSubmit, breakpointEnter/Exit) and
  `effects` (toggleVisibility, scrollToElement, mirrorMouseMovement), each with
  its extra `settings` keys.
- `novamira/divi-set-interactions` — set the interactions on the module at
  `address`. `interactions` is a list of `{trigger, effect, settings?}`; the
  list replaces the module's current interactions. Interactions are **desktop
  only** (not responsive) and **target the module itself**. Example —
  `[{"trigger":"onClick","effect":"toggleVisibility"}]` toggles the module when
  clicked.

## Divi Library (reusable layouts)

Save a section, row, module, or whole page to the Divi Library and reuse it
across pages:

- `novamira/divi-list-library-items` — the saved items (id, title, type,
  `global`).
- `novamira/divi-create-library-item` — save the subtree at an `address` on a
  `post` (omit `address` to save the whole page) under a `title`; returns the
  `library_id`. Pass `global: true` for a **Global** item (live-linked).
- `novamira/divi-apply-library-item` — insert a saved item into a page at a
  `parent` address (omit for the top level — a saved section). A regular item is
  copied (nesting validated); a **Global** item is inserted as a live-linked
  reference, so editing the saved item later updates every page using it.
- `novamira/divi-delete-library-item` — remove a saved item.

A library item id is an ordinary Divi post — edit its content with the normal
content/module abilities by passing the id as the post. Editing a **Global**
item's content propagates to every page that references it.

## Theme Builder (header / footer / body)

Build custom headers, footers, and body/archive/404 layouts that apply by
condition:

- `novamira/divi-list-theme-builder-templates` — list templates with their
  header/body/footer **layout post ids** and assignment conditions (`use_on` /
  `exclude_from`).
- `novamira/divi-list-theme-builder-conditions` — the catalog of valid
  assignment condition ids (e.g. `singular:post_type:page:all`, `homepage`,
  `archive:post_type:post`); a `dynamic` id ending in `:` needs an object id
  appended.
- `novamira/divi-create-theme-builder-template` — create a template: pick
  `areas` (`header`/`body`/`footer`), set `use_on`/`exclude_from`, optionally
  `default: true`. Returns the new layout post ids.
- `novamira/divi-set-theme-builder-conditions` — re-assign (`use_on` /
  `exclude_from` replace the lists) or enable/disable an existing template.
- `novamira/divi-delete-theme-builder-template` — remove a template (and, by
  default, its layouts).

**The header/body/footer layout ids are ordinary Divi posts.** After creating a
template, build each returned layout id with the normal content/module abilities
(`divi-set-content`, `divi-add-module`, …) passing the layout id as the post.
The template applies on the front end immediately.

## Gotchas

- **Responsive wrapper required.** Most attributes need the
  `<breakpoint>.value` shape (at least `desktop`). A bare scalar is ignored.
- **Some content fields take a structured value, not a string.** The button's
  `button.innerContent.<breakpoint>.value` is an object `{text, linkUrl, …}`
  (a plain string renders an empty button). When a module doesn't show the
  content you set, check whether the field expects sub-keys.
- **Structural nesting is enforced.** A leaf module placed directly in a section
  (skipping row/column) will not render correctly.
- **Cache after structural writes.** `update_post_meta` does not fire
  `save_post`; structural writes should `clean_post_cache($post_id)`.
- **Divi 4 shortcodes are out of scope.** These abilities operate on the Divi 5
  block model only.

> Use `novamira/divi-list-modules` to see which abilities are currently
> registered on this site.
