Building Dynamic Gutenberg Blocks inside Awesome XP
With Awesome XP you can build fully native Gutenberg blocks without Advanced Custom Fields or a build step. Layout, controls, CSS, JavaScript and server-side rendering all live in WordPress, in one post under Add Awesome Gutenberg Blocks. Each block is a set of templates: Register, Controls, Render, Styles and Scripts.
Compatibility: the block style in this article (Styles and Scripts templates,
row_repeater,innerblocks,selectandtogglecontrols, sidebar sections) works in Awesome XP only. It is not supported in the Awesome Enterprise WP plugin. The basic three-template block (Register, Render, Controls withtext,titleandtextareafields) is the older pattern from version 3.2.2.
What works where
| Feature | Awesome XP | Awesome Enterprise WP plugin |
|---|---|---|
| Register, Render and Controls templates | Supported | Supported (3.2.2) |
styles_service and scripts_service |
Supported | Not supported |
description in block config |
Supported | Not supported |
select and toggle controls |
Supported | Not supported |
Sidebar sections ([sidebar _value="true" /]) |
Supported | Not supported |
row_repeater with table layout |
Supported | Not supported |
innerblocks field |
Supported | Not supported |
1. The Register template
Register maps the block to its templates. Two things changed from the older pattern: the config takes a description, and there are two new services, styles_service and scripts_service.
Important: the
namein the config must match the middle part of every service path. A block namedawx-testimonial-cardusesgb_blocks.awx-testimonial-card.render,.controls,.stylesand.scripts. If they don’t match, the block can’t find its templates.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[templates.add register] [gt_blocks.register] [config] [name]awx-testimonial-card[/name] [title]AWX Testimonial Card[/title] [icon]format-quote[/icon] [category]common[/category] [description]Modern testimonial card testing SSR bindings, cascade layers, and compiled hashed assets.[/description] [/config] [render_service _value="gb_blocks.awx-testimonial-card.render" /] [controls_service _value="gb_blocks.awx-testimonial-card.controls" /] [styles_service _value="gb_blocks.awx-testimonial-card.styles" /] [scripts_service _value="gb_blocks.awx-testimonial-card.scripts" /] [/gt_blocks.register] [/templates.add] |
Both modules use the wrapper class wp-block-dgb-<name> in their Render template, for example wp-block-dgb-awx-testimonial-card. All your CSS and JavaScript selectors hang off that class.
2. The Controls template
Controls define the editor UI and the block’s data. Each field writes to a key under content.* or style.* through attr_name, and Render reads the same keys back. Fields are grouped into sections, and every field can carry a default.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
[templates.add controls] [arr.create o.set='template.controls'] [sections new] [name _value='section_content' /] [title _value="Content" /] [icon _value="edit" /] [fields new] { "type": "title", "name": "author", "label": "Author Name", "attr_name": "content.author", "default": "Sarah Jenkins", "validation": { "required": true } } [/fields] [fields new] { "type": "textarea", "name": "quote", "label": "Testimonial Quote", "attr_name": "content.quote", "default": "The speed and modular design transformed our workflow." } [/fields] [/sections] [sections new] [name _value='card_design' /] [title _value="Card Appearance" /] [icon _value="admin-settings" /] [sidebar _value="true" /] [fields new] { "type": "select", "name": "card_style", "label": "Card Style", "attr_name": "style.card_style", "default": "card--bordered", "options": [ { "label": "Bordered Outline", "value": "card--bordered" }, { "label": "Elevated Shadow", "value": "card--elevated" }, { "label": "Minimal Muted", "value": "card--minimal" } ] } [/fields] [/sections] [/arr.create] [template.return template.controls /] [/templates.add] |
Field types used in the modules
| Type | Use it for | Notes |
|---|---|---|
text |
Short single-line values | Good for badges, roles, extra CSS classes |
title |
Main heading or name | Supports "validation": { "required": true } |
textarea |
Longer text | Quotes, descriptions |
select |
Pick one option | Takes an options array of label and value. Store CSS class names as values and drop them straight into the markup |
toggle |
On or off | "default": true |
row_repeater |
A list of rows | See below |
innerblocks |
Nested WordPress blocks | See below |
A section with [sidebar _value="true" /] appears in the settings sidebar.
Repeating rows with row_repeater
row_repeater stores an array of rows at its attr_name. Each column is a repeater_fields entry with its own width, and "layout": "table" edits the rows as a table. The default array seeds the first rows.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[fields new] { "type": "row_repeater", "name": "features", "label": "Feature Highlights", "attr_name": "content.features", "layout": "table", "repeater_fields": [ { "type": "text", "name": "icon", "label": "Icon / Emoji", "width": "15%", "default": "⚡" }, { "type": "text", "name": "title", "label": "Title", "width": "30%", "default": "Lightning Fast" }, { "type": "text", "name": "desc", "label": "Description", "width": "40%", "default": "Extracted and compiled with @layer scoping." }, { "type": "text", "name": "badge", "label": "Tag", "width": "15%", "default": "Core" } ], "default": [ { "icon": "⚡", "title": "Compiled CSS & JS", "desc": "Extracted with content-hashed disk caches.", "badge": "Core" } ] } [/fields] |
Nested blocks with innerblocks
An innerblocks field gives editors a slot for any WordPress block, such as buttons, paragraphs or media. It needs only a label and an attr_name.
|
1 2 3 4 5 6 7 8 |
[fields new] { "type": "innerblocks", "name": "call_to_action", "label": "Call-to-Action Area (Buttons, Links, Media)", "attr_name": "content.innerblocks" } [/fields] |
3. The Render template
Render runs on the server and returns the block’s front-end HTML. Read each value with [template.get] using the same content.* or style.* key you set in Controls, and give it a default so empty blocks still render. Use o.set to park a value in a template variable.
Print [template.get wp.wrapper_attributes /] on the outer element so WordPress can add its own block attributes.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
[templates.add render] [template.get content.author default="Anonymous" o.set="template.author" /] [template.get content.role default="" o.set="template.role" /] [template.get content.quote default="" o.set="template.quote" /] [template.get style.card_style default="card--bordered" o.set="template.style" /] [template.get style.author_class default="" o.set="template.author_cls" /] <div class="wp-block-dgb-awx-testimonial-card" [template.get wp.wrapper_attributes /]> <div class="testimonial-inner [template.get style /]"> [if.not_empty template.quote] <blockquote class="testimonial-quote"> “[template.get quote /]” </blockquote> [/if.not_empty] <div class="testimonial-footer"> [if.not_empty template.author] <span class="testimonial-author [template.get author_cls /]"> [template.get author /] </span> [/if.not_empty] [if.not_empty template.role] <span class="testimonial-role"> [template.get role /] </span> [/if.not_empty] </div> </div> </div> [/templates.add] |
The select value goes straight into the class list (testimonial-inner card--elevated), so the style options in Controls and the modifier classes in Styles must use the same names.
Looping over a row_repeater
Guard the loop with c.not_empty, then iterate with [loop.@f ...]. Inside the loop, read every column through .item: @f.item.title, never @f.title.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[do.@ig c.not_empty=template.features] <div class="awx-fs-grid"> [loop.@f template.features] <div class="awx-fs-card"> [if.true template.show_icons] [if.not_empty @f.item.icon] [/if.not_empty] [/if.true] [if.not_empty @f.item.title] [/if.not_empty] [if.not_empty @f.item.desc] [/if.not_empty] </div> [/loop.@f] </div> [/do.@ig] |
Printing nested blocks
The innerblocks field is output with a plain [template.get] on its attr_name. Put it inside a wrapper you can style.
|
1 2 3 4 5 |
<div class="awx-fs-footer"> <div class="awx-fs-innerblocks-wrapper"> [template.get content.innerblocks /] </div> </div> |
A toggle such as style.show_icons is read the same way and tested with [if.true ...]. The showcase module sets its default to "1" in Render, alongside the toggle’s true default in Controls.
4. The Styles template
Styles hold plain CSS for the block, with no shortcodes. Awesome XP extracts it from the post and compiles it into a content-hashed file on disk.
Three rules keep it safe:
- Scope every selector under the block’s wrapper class, such as
.wp-block-dgb-awx-testimonial-card, so styles can’t leak into the rest of the page. - **Use
--awx-*tokens with fallbacks** for spacing, colour, radius and type, so the block follows your Awesome CSS design system and still looks right without it. - Match the modifier classes to the values in your
selectfields.card--bordered,card--elevatedandcard--minimalbelow come straight from the Card Style options.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
[templates.add styles] .wp-block-dgb-awx-testimonial-card { display: block; box-sizing: border-box; margin-block: var(--awx-space-4, 1.5rem); } .wp-block-dgb-awx-testimonial-card .testimonial-inner { padding: var(--awx-space-4, 1.5rem); border-radius: var(--awx-radius-md, 8px); background-color: var(--awx-color-surface, #ffffff); color: var(--awx-color-text, #1e293b); transition: transform var(--awx-duration-normal, 200ms) var(--awx-ease-out, ease-out), box-shadow var(--awx-duration-normal, 200ms) var(--awx-ease-out, ease-out); cursor: pointer; } /* Modifiers driven by style.card_style */ .wp-block-dgb-awx-testimonial-card .card--bordered { border: var(--awx-border-thin, 1px) solid var(--awx-color-border, #e2e8f0); } .wp-block-dgb-awx-testimonial-card .card--elevated { border: none; box-shadow: var(--awx-shadow-md, 0 4px 6px -1px rgba(0, 0, 0, 0.1)); } .wp-block-dgb-awx-testimonial-card .card--minimal { border: none; background-color: var(--awx-color-surface-subtle, #f8fafc); } /* State toggled by the Scripts template */ .wp-block-dgb-awx-testimonial-card .testimonial-inner.is-highlighted { transform: translateY(-2px); box-shadow: var(--awx-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1)); } [/templates.add] |
Cascade layers
The showcase module wraps all its CSS in @layer blocks { ... }, so the block’s rules sit in a known layer and are easy to override from your theme. Wrap yours the same way when you want that control.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
[templates.add styles] @layer blocks { .wp-block-dgb-awx-features-showcase .awx-fs-grid { display: grid; grid-template-columns: 1fr; gap: var(--awx-fs-gap); } @media (min-width: 640px) { .wp-block-dgb-awx-features-showcase.has-cols-3 .awx-fs-grid { grid-template-columns: repeat(3, 1fr); } } } [/templates.add] |
The column count works the same way as the card style: the select value 3 becomes the class has-cols-3 in Render, and the CSS reacts to it.
5. The Scripts template
Scripts hold plain front-end JavaScript, with no shortcodes and no <script> tags. Like Styles, it is compiled to a hashed file. Scope your selectors to the block’s wrapper class and wait for DOMContentLoaded.
This script toggles the is-highlighted class from Styles when a visitor clicks a testimonial.
|
1 2 3 4 5 6 7 8 9 10 |
[templates.add scripts] document.addEventListener('DOMContentLoaded', function () { const cards = document.querySelectorAll('.wp-block-dgb-awx-testimonial-card .testimonial-inner'); cards.forEach(function (card) { card.addEventListener('click', function () { card.classList.toggle('is-highlighted'); }); }); }); [/templates.add] |
Keep the visual state in CSS classes, as here, rather than writing inline styles from JavaScript. The showcase module does set borderColor inline on hover, but a class is easier to theme and override.
Complete testimonial module
Paste this into a single post under Add Awesome Gutenberg Blocks in Awesome XP. It will not work in the Awesome Enterprise WP plugin.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
[templates.add register] [gt_blocks.register] [config] [name]awx-testimonial-card[/name] [title]AWX Testimonial Card[/title] [icon]format-quote[/icon] [category]common[/category] [description]Modern testimonial card testing SSR bindings, cascade layers, and compiled hashed assets.[/description] [/config] [render_service _value="gb_blocks.awx-testimonial-card.render" /] [controls_service _value="gb_blocks.awx-testimonial-card.controls" /] [styles_service _value="gb_blocks.awx-testimonial-card.styles" /] [scripts_service _value="gb_blocks.awx-testimonial-card.scripts" /] [/gt_blocks.register] [/templates.add] [templates.add controls] [arr.create o.set='template.controls'] [sections new] [name _value='section_content' /] [title _value="Content" /] [icon _value="edit" /] [fields new] { "type": "title", "name": "author", "label": "Author Name", "attr_name": "content.author", "default": "Sarah Jenkins", "validation": { "required": true } } [/fields] [fields new] { "type": "text", "name": "role", "label": "Author Role / Company", "attr_name": "content.role", "default": "VP of Engineering at Acme Corp" } [/fields] [fields new] { "type": "textarea", "name": "quote", "label": "Testimonial Quote", "attr_name": "content.quote", "default": "The speed and modular design transformed our workflow. Everything renders reliably with zero runtime overhead." } [/fields] [/sections] [sections new] [name _value='card_design' /] [title _value="Card Appearance" /] [icon _value="admin-settings" /] [sidebar _value="true" /] [fields new] { "type": "select", "name": "card_style", "label": "Card Style", "attr_name": "style.card_style", "default": "card--bordered", "options": [ { "label": "Bordered Outline", "value": "card--bordered" }, { "label": "Elevated Shadow", "value": "card--elevated" }, { "label": "Minimal Muted", "value": "card--minimal" } ] } [/fields] [fields new] { "type": "text", "name": "author_class", "label": "Author Extra Class", "attr_name": "style.author_class", "default": "has-accent-color" } [/fields] [/sections] [/arr.create] [template.return template.controls /] [/templates.add] [templates.add styles] .wp-block-dgb-awx-testimonial-card { display: block; box-sizing: border-box; margin-block: var(--awx-space-4, 1.5rem); } .wp-block-dgb-awx-testimonial-card .testimonial-inner { padding: var(--awx-space-4, 1.5rem); border-radius: var(--awx-radius-md, 8px); background-color: var(--awx-color-surface, #ffffff); color: var(--awx-color-text, #1e293b); transition: transform var(--awx-duration-normal, 200ms) var(--awx-ease-out, ease-out), box-shadow var(--awx-duration-normal, 200ms) var(--awx-ease-out, ease-out); position: relative; cursor: pointer; } /* Modifiers driven by style.card_style */ .wp-block-dgb-awx-testimonial-card .card--bordered { border: var(--awx-border-thin, 1px) solid var(--awx-color-border, #e2e8f0); } .wp-block-dgb-awx-testimonial-card .card--elevated { border: none; box-shadow: var(--awx-shadow-md, 0 4px 6px -1px rgba(0, 0, 0, 0.1)); } .wp-block-dgb-awx-testimonial-card .card--minimal { border: none; background-color: var(--awx-color-surface-subtle, #f8fafc); } .wp-block-dgb-awx-testimonial-card .testimonial-quote { font-size: var(--awx-font-size-2, 1.125rem); line-height: var(--awx-leading-relaxed, 1.6); font-style: italic; margin-block-end: var(--awx-space-3, 1rem); } .wp-block-dgb-awx-testimonial-card .testimonial-footer { display: flex; flex-direction: column; gap: var(--awx-space-1, 0.25rem); border-top: 1px solid var(--awx-color-border, #f1f5f9); padding-top: var(--awx-space-2, 0.5rem); } .wp-block-dgb-awx-testimonial-card .testimonial-author { font-weight: var(--awx-font-weight-bold, 600); font-size: var(--awx-font-size-1, 1rem); } .wp-block-dgb-awx-testimonial-card .testimonial-author.has-accent-color { color: var(--awx-color-primary, #2563eb); } .wp-block-dgb-awx-testimonial-card .testimonial-role { font-size: var(--awx-font-size-0, 0.875rem); color: var(--awx-color-text-secondary, #64748b); } /* Interactive state */ .wp-block-dgb-awx-testimonial-card .testimonial-inner.is-highlighted { transform: translateY(-2px); box-shadow: var(--awx-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1)); } [/templates.add] [templates.add scripts] document.addEventListener('DOMContentLoaded', function () { const cards = document.querySelectorAll('.wp-block-dgb-awx-testimonial-card .testimonial-inner'); cards.forEach(function (card) { card.addEventListener('click', function () { card.classList.toggle('is-highlighted'); }); }); }); [/templates.add] [templates.add render] [template.get content.author default="Anonymous" o.set="template.author" /] [template.get content.role default="" o.set="template.role" /] [template.get content.quote default="" o.set="template.quote" /] [template.get style.card_style default="card--bordered" o.set="template.style" /] [template.get style.author_class default="" o.set="template.author_cls" /] <div class="wp-block-dgb-awx-testimonial-card" [template.get wp.wrapper_attributes /]> <div class="testimonial-inner [template.get style /]"> [if.not_empty template.quote] <blockquote class="testimonial-quote"> “[template.get quote /]” </blockquote> [/if.not_empty] <div class="testimonial-footer"> [if.not_empty template.author] <span class="testimonial-author [template.get author_cls /]"> [template.get author /] </span> [/if.not_empty] [if.not_empty template.role] <span class="testimonial-role"> [template.get role /] </span> [/if.not_empty] </div> </div> </div> [/templates.add] |