Builder internals
How the builder stores pages, renders them and loads element modules. For a step-by-step element, see adding an element.
Where the data lives
| Post meta | Content |
|---|---|
_bluetheme_builder |
The page document as JSON (below). Kept with every revision. |
_bluetheme_builder_active |
true when the page is shown with the builder. |
_bluetheme_builder_css |
Hash of the page's generated CSS file. |
Builder pages get wp-content/uploads/bluetheme/css/post-{id}.css. The file is rebuilt when the document is saved, and again on view when it is missing or out of date.
Document
{
"version": 1,
"sections": [
{
"id": "a1b2c3d4", "type": "section", "settings": { },
"children": [
{
"id": "e5f6g7h8", "type": "wrap", "settings": { "size": { "desktop": "1/2", "mobile": "1/1" } },
"children": [
{ "id": "i9j0k1l2", "type": "element", "element": "heading", "settings": { "title": "Hello" } }
]
}
]
}
]
}
- Sections hold wraps. Wraps hold elements and other wraps, nested up to 4 levels deep.
- Ids are 4–16 lowercase letters or digits, unique within the page.
settingsstores only values that differ from the defaults. Per-device values are maps such as{ "desktop": 40, "mobile": 24 }, and each smaller device inherits from the next larger one.- Wrap sizes are
1/6 … 1/1or a percentage such as"37.5%". On mobile, wraps are full width unless a mobile size is set. - Every save goes through
BlueTheme\Builder\Data\Sanitizer. It rebuilds the tree, keeps only known settings, and cleans each value with the Theme Options field sanitizer. HTML is filtered unless the user hasunfiltered_html. - When the format changes, raise
Migrations::VERSIONand add an upgrade step.
Element modules
An element is a folder, found in bluetheme/elements/, in the child theme's elements/ (which can override a theme element of the same name), or in any folder added with the bluetheme_builder_element_dirs filter.
elements/my-element/
config.json required — title, description, category, icon, keywords, summary, fields
render.php required — prints the element's inner HTML
style.css optional — loaded only on pages that use the element
script.js optional — same rule; loaded deferred, in the footer
config.json
{
"title": "Heading",
"description": "A heading (H1–H6) or a line of text, with an optional link.",
"category": "typography",
"icon": "bi:type-h1",
"keywords": [ "title" ],
"summary": "title",
"textdomain": "bluetheme",
"fields": [
{ "type": "text", "id": "title", "label": "Title", "default": "Heading", "translate_default": true },
{
"type": "number", "id": "size", "label": "Font size", "default": "", "responsive": true,
"unit": "px", "tab": "style",
"css": { "selector": "{{WRAPPER}} .bt-heading", "property": "font-size", "unit": "px" }
}
]
}
- description: one sentence, shown as the tooltip in the Elements tab and in the elements reference.
- category: one of
typography,media,boxes,interactive,sliders,dynamic,post,woocommerce,product,header,other. - icon: any icon reference the Theme Options icon picker accepts, as
pack:name. - summary: the field whose text the backend builder shows on the element's card.
- fields: use the Theme Options field types: text, textarea, html, url, number, select, switch, color, image, icon, dimensions, background, code and the others.
- Extra keys:
tab:content,styleoradvanced.responsive: true for per-device values.show_if: shows the field only under a condition.translate_default: the default text is translated.
- Shared fields: every element also gets padding, margin, background, text color, radius, CSS id and classes, hide on device, and custom CSS.
- Extra keys:
- css: turns a field into CSS. It can be one rule or a list of rules.
- Each rule has a
selector(where{{WRAPPER}}stands for the element's own class), plus either apropertyor a CSS custom property invar. - Optional keys: a
unit, and amapthat translates stored values into CSS values. - Output: CSS is produced only for values that differ from the defaults, so put the defaults in
style.css.
- Each rule has a
- Translations: labels are written in English.
pnpm i18nrunstools/element-strings.mjs, which lists them for translators. Third-party elements set their owntextdomain.
Libraries (requires)
An element that needs a shared library lists it in config.json:
"requires": [ "leaflet" ]
The library loads only on pages that use the element, before the element's script.js. Every library is bundled with the theme (pnpm vendor copies them into assets/vendor/); nothing loads from a CDN.
Elements for a plugin (needs)
An element that only works with a plugin names it, and is left out of the builder (and not rendered) while that plugin is off:
"needs": "woocommerce"
woocommerce is the one key the theme knows; the bluetheme_builder_element_plugin_active filter (active, key) answers for others.
Reusing another element's assets (uses)
An element that reuses the stylesheet and script of another element names it in config.json, for example Archive Posts, which uses the Blog post cards:
"uses": [ "blog" ]
| Name | Library | Used by |
|---|---|---|
lightbox |
The theme's own lightbox (src/frontend/lightbox.js) |
Image, Image Gallery, Zoom Box |
countup |
The theme's count-up and progress script (src/frontend/countup.js) |
Counter, Quick Fact, Progress Bars |
leaflet |
Leaflet 1.9 | Map Advanced |
lottie |
lottie-web (light build) | Lottie |
chartjs |
Chart.js 4 | Chart |
swiper |
Swiper | (through slider) |
slider |
The theme's slider set-up (src/frontend/slider.js); loads swiper first |
All slider elements |
Add more with the bluetheme_builder_vendors filter.
Lightbox: any link with data-bt-lightbox="group" opens its href in the lightbox. Links with the same group form a gallery. The caption comes from data-bt-caption, else from the image's alt text.
Repeaters and galleries
- Repeater fields: their
fieldsand default rows are translated like other fields.- Default rows need a fixed
_id, so saved settings that equal the defaults are recognized and dropped. - Mark row fields with
translate_defaultto translate the texts of the default rows.
- Default rows need a fixed
galleryfield: stores a list of{ id, url }images.- Site-dependent choices:
- A select field with
"choices_from": "forms"gets its choices from thebluetheme_builder_choicesfilter. - Built in:
forms, the Contact Form 7 and WPForms forms, ascf7:IDandwpforms:ID.
- A select field with
- Translatable defaults: defaults marked
translate_defaultare listed for translators even when they have no letters, such as a currency sign.
Element scripts
- Element scripts are plain JavaScript; they are not bundled, and
pnpm lint:jschecks them. - After the live builder replaces a section, it fires
bluetheme:render. Scripts that set things up per element should listen for it. - Skip clicks that would leave the page while
body.bt-canvasis set (the live builder canvas).
Helpers for render.php
Elements\Parts: escaped markup for the pieces most elements repeat:- images from an image field;
- headings with an allowed tag, linked or not;
- textarea text, links and theme buttons;
tel:links from phone numbers typed with Persian digits;- count-up numbers (
Parts::count), and number or label lists separated by commas, Persian commas included (Parts::numbers,Parts::labels); - the live-builder hint (
Parts::hint).
Elements\Embed: reads YouTube, Vimeo and Aparat links or ids and builds player addresses (YouTube uses its no-cookie domain).Elements\Maps:- the tile server, attribution and click-to-load settings from Theme Options → Global → Addons;
- a coordinate parser that accepts Persian digits.
Renderer::$editing: true inside the live builder. Elements use it to show a hint instead of nothing when required content is missing.
render.php
These variables are available:
$settings: the saved values merged over the defaults.$node: the element'sid,typeandelementname.$element: theBlueTheme\Builder\Elements\Elementobject.
Print only the element's inner markup and escape everything. The renderer wraps it in <div class="bt-el bt-el-{name} bt-n-{id}">.
REST API (bluetheme/v1, needs edit_post)
| Route | Purpose |
|---|---|
GET /builder/{id} |
Document and active flag |
POST /builder/{id} |
Save { document, active } and record a revision |
POST /builder/{id}/render |
HTML and CSS of one node (previews) |
GET /builder/{id}/revisions |
Revisions that contain builder data |
GET /builder/{id}/revisions/{rev} |
One revision's document |
The block editor and the classic editor save through the post itself; they don't use these routes.
Shared Style and Advanced settings
Every section, wrap and element gets these settings (StyleFields, AdvancedFields):
- Style: typography, text color and text color on hover, background and background color on hover, padding and margin (per device), border style, width and color (with a hover color), corner radius (per device), and box shadow (with a hover shadow).
- Advanced: CSS id and classes, an entrance animation with delay and duration, sticky (top or bottom, with an offset), z-index, hide on device, display conditions, and custom CSS.
- Display conditions: show to everyone, only to logged-in users (optionally only some roles), or only to visitors who are not logged in; and show from / until dates in the site's time zone. Conditions are checked when the page renders, so a page cache must not serve cached pages to logged-in users.
An element can point a shared style at an inner part with style_selectors in config.json:
"style_selectors": {
"typography": "{{WRAPPER}} .bt-heading",
"text_color": "{{WRAPPER}} .bt-heading",
"hover_color": "{{WRAPPER}}:hover .bt-heading"
}
Section overlay, video, dividers and slider
Sections have these settings too (Data\SectionFields, rendered by Render\SectionExtras):
- Overlay (Style): a color, gradient or image over the background and the video, under the content, with its own opacity (default 60%).
- Background video (Style):
- Sources: a video file from the Media Library, or an Aparat, YouTube or Vimeo link. It plays muted, in a loop, without controls.
- The poster image shows until the video plays. It is also all that shows:
- on phones, unless Play the video on phones is on;
- for visitors who prefer less motion;
- for embedded players while the cookie notice waits for consent, when Theme Options → GDPR blocks external embeds. The video starts on the next page view after consent.
- The video is added by a script when the section comes near the screen.
- Whether Aparat honors autoplay and mute inside a frame is up to Aparat's player.
- Shape dividers (Style): top and bottom, in ten shapes (
Render\Dividers), each with a color (usually the next section's background), a height per device and a flip.- The shapes are drawn for the bottom edge; the top one is turned upside down.
- Right-to-left pages mirror them, so a shape leans the same way relative to the reading direction.
- Slider (Content): the section's wraps become Swiper slides, with slides at once (per device), slide or fade, arrows, position indicator, autoplay with a pause button, and loop. The space between slides is the section's Space between wraps.
- In the live builder the wraps stay side by side, marked with ⇆, so they can still be edited.
Layers inside the section, in order: .bt-section__video, .bt-section__overlay, the top divider, .bt-section__inner (the content, above the others), and the bottom divider.
Internal modules: the video and slider scripts load like element assets. Tree::element_names() lists the internal section-video and section-slider modules when a section needs them, so every page, template and preview that renders such a section loads them. Modules with "internal": true in config.json are not offered in the Elements panel.
Pre-built sections
The builder's Pre-built sections tab offers 30 ready-made sections in ten categories (three each): Hero, About, Services, Features, Team, Pricing, Testimonials, Call to action, Contact, and Numbers & clients.
- Inserting: clicking a card adds an editable copy at the end of the page; Undo removes it. The tab has category chips and a search box (by section name).
- Previews: each card shows the real section rendered with the site's styles at desktop width, scaled down. It uses the preset preview (
?bt_preset=section:{id}, for users who can publish pages), loaded when the card scrolls into view. - Following the site:
- Colors refer to the palette (
palette:primary,var(--bt-palette-secondary)in gradients), so sections take Theme Options → Colors. - Images are the theme's own illustrations in
assets/images/library/. - All texts are translatable. The sample price currency is translated too (in Persian: «هزار تومان»).
- Colors refer to the palette (
- Code:
Library\Library: the registry and routes (GET bluetheme/v1/library,GET bluetheme/v1/library/{id}, needsedit_posts).Library\Kit: the shared building blocks.Library\Sections\*: one class per category.- Plugins and child themes add sections with the
bluetheme_library_sectionsfilter (id => [ label, category, section node ]).
- Dark sections:
Kit::dark()sets white text and lightens--bt-color-muted. It also gives slider arrows a see-through background.- Sections with light cards on a dark background (pricing) color only their intro.
- Eyebrow labels use the accent color on dark backgrounds.
- Readable widths: centered intros keep their content at 800 px with
selector > .bt-wrap__inner. The wrap itself stays full width, so the next wrap starts a new line. bt-buttons-inline: this CSS class on a wrap (Advanced → CSS classes) puts its buttons side by side. The hero sections use it, and it works on any wrap.- No letter spacing on labels: it would break the joined letters of Persian text.
Live builder
- Where it is:
admin.php?page=bluebuilder&post={id}. It's linked from the block editor ("Edit live"), the backend builder, the posts list and the admin bar. - The canvas: the real page in an iframe, in canvas mode (
?bluebuilder_canvas=1&_wpnonce=…, only for users who can edit the post).- Nodes carry
data-bt-idanddata-bt-type. - Display conditions are ignored, so editors see every item.
- Every element's assets are loaded.
- Nodes carry
- Rendering changes: after each change, the builder sends only the sections that changed to
POST /builder/{id}/render-sectionsand swaps the returned HTML and CSS into the canvas. - Element scripts: after a section is replaced, the canvas dispatches a
bluetheme:renderevent on it. Element scripts should listen for it and re-initialize, for example sliders. - Saving:
- Update saves through
POST /builder/{id}and records a revision. - Autosave runs every 60 seconds while there are unsaved changes, into the user's WordPress autosave revision.
- Preview autosaves, then opens
?bluebuilder_preview=1&_wpnonce=…, which shows that autosave to the current user only.
- Update saves through
Sliders and post types
- Slider elements: they use
Elements\Slider. In render.php, callopen( $settings ), thenslide( $html )for each slide, thenclose( $settings ).- Shared settings:
per_viewandgap(per device),arrows,dots,autoplay,delay,loop,speedandeffect. - Breakpoints: per-device values become Swiper breakpoints built from the theme's device breakpoints.
- Pause button: autoplay always comes with one.
- Reduced motion: for visitors who ask for less motion, autoplay and sliding are off.
- Shared settings:
- Post types: Portfolio (
portfolio), Clients (bt_client), Testimonials (bt_testimonial), Offers (bt_offer) and Slides (bt_slide), each with a category taxonomy.- Extra fields: the extra fields are post meta named
_bluetheme_{field}(PostTypes\Fields). They are edited in a "Details" box. - Turning them off: each type can be turned off in Theme Options → Global → Performance → "Disable features".
- Extra fields: the extra fields are post meta named
- Items from either source:
Elements\Items::get( $kind, $settings )returns the same item shape whether the element reads posts (source=posts, withcategory,count,orderby) or items entered in the element (source=manual). - Cards:
Elements\Cardsprints post and project cards for sliders and lists.
Lists
- Paginated list elements (Blog, Portfolio) use
Lists\Pager.- Each list reads its page from its own query argument,
bt_p_{node id}, so several lists on one page are independent. - The controls are plain links, so they work without JavaScript.
- Each list reads its page from its own query argument,
- The
listslibrary (src/frontend/lists.js) loads the next page in place for "Load more" and infinite scroll. It filters items bydata-terms, and swaps the list fordata-ajaxfilter links (portfolio archive). - Mark-up a list needs: a wrapper with
data-bt-list="{key}", the items inside[data-bt-list-items], and the parts printed byPager::controls()andPager::status(). - Portfolio grids:
Portfolio\Gridprints them, for both the Portfolio element and the portfolio archive. Their styles are in the main stylesheet (_portfolio.scss).
Templates
- Post type and edit screen: templates are posts of type
bt_template. They are built with BlueBuilder; "Edit" in the Templates list opens the live builder. - Types: stored in
_bluetheme_template_typeand listed inTemplates\Types. Thebluetheme_template_typesfilter adds types. - Front address (
/bt-template/…): it exists only for the builder canvas. Visitors get a 404, and search engines anoindex. - Display conditions:
- Stored in
_bluetheme_conditions(JSON) and_bluetheme_priority. - Rules and specificity are documented in
Templates\Conditions. Templates\Resolver::get( $type )picks the template for the current request. The order is: the template's own address, then the page's own choice (_bluetheme_template_{type}: an id, or -1 for none), then the best match. Thebluetheme_templatefilter changes the result.
- Stored in
- Locations:
bluetheme_location( 'header' | 'footer' )prints Elementor Pro's location or the matching template.Templates\Locations::html( $id )prints any template. It adds editing markers only for the template open in the builder, and it guards against a template embedding itself.
- Global sections:
- The Template element shows a Section template.
- The templates a page embeds are queued with the page's assets, through the
bluetheme_builder_enqueuedaction.
- REST (
bluetheme/v1/templates…): list, create ("Save as template"), a template's layout, display settings, and condition rules and search. - Import and export:
Templates\Transferreads and writes.jsonfiles. Imported layouts are cleaned with the importing user's permissions.
Header templates
- Rows and zones: a header is ordinary sections. A section's "Wrap layout" set to zones (
row_layout) puts the first wrap at the start, the last at the end and a middle one in the center, with the zone's elements side by side. Rows for one device use "Hide on" (hide_on); "Hide this row when the header sticks" (sticky_hide) suits top bars. - Starter layout: a new, empty header gets
Templates\Starters::document( 'header' ): a top bar and a main row for desktop and laptop, and a logo, search and burger row for tablet and mobile. - Behavior settings: stored in
_bluetheme_header(JSON) and documented inTemplates\Header. They cover sticky (off, sticky, smart), the offset, shrink, shadow, sticky colors, and transparent with its text color. They are edited in the builder's template settings tab and saved through the template settings REST route.Header::attributes()turns them into classes anddata-bt-sticky*attributes forsrc/frontend/header.js. - Header elements (category
header): Logo, Menu, Burger Menu, Search, Cart, Account, Wishlist, Language Switcher, Social Icons, Header Text, Copyright, Dark Mode Toggle. The burger and search buttons open the shared#bt-menu-paneland#bt-search-panel, printed once byHeader::panels().Parts::icon_link()renders the icon links (cart, account, wishlist).
Footer templates and ready-made templates
- Ready-made templates:
Templates\Presets\Headers(16) andTemplates\Presets\Footers(11) write builder documents with the helpers inPresets\Build(row(),section(),wrap(),el(),mobile_row()and so on).Presets\Presetslists them by type, with gallery categories, and thebluetheme_template_presetsfilter adds or removes presets. A header preset may carry header settings (side layout, transparent). - Gallery: the "Add template" page shows the gallery of the picked type (
Presets\Gallery,src/admin/templates.js). Each card loads a live preview,/?bt_preset=header:classic(Presets\Preview, for users who can publish pages), in a scaled frame once the card is on screen. "Blank" starts with an empty template; new headers and footers made elsewhere start from the default preset (Presets::DEFAULTS, throughStarters). - Footer settings: stored in
_bluetheme_footer(Templates\Footer). Thestylesetting can beinherit(Theme Options → Footer),default,stickyorreveal, and it reaches the existing footer body classes through thebluetheme_footer_stylefilter. Footer templates also print the back-to-top button. - Side header: the header setting
layoutcan betop,side-startorside-end, with aside_width. From 992px up the header is a fixed vertical bar and the page is padded on that side; its last section is pushed to the bottom. Below 992px it is an ordinary top header. - Row text color: a section's or wrap's text color also sets
--bt-color-headingand--bt-color-menu-link, so headings and menus follow it. Footer templates color header-text links with the row's text color too.
Mega menus and side panels
- Mega menu: a top-level menu item can open a Mega menu template. The template is picked under Appearance → Menus → the item → "Mega menu", stored in the item meta
_bluetheme_megamenu. In horizontal desktop menus,Template\MenuWalkerprints the template's panel (MegaMenu::panel()) in place of the item's submenu, with the same toggle button, sosrc/frontend/nav.jsopens it by click, hover, keyboard and Escape like a submenu. Vertical menus and mobile menus keep the normal submenu. - Panel width: the template setting
width(meta_bluetheme_megamenuon the template) can beinherit(Theme Options → Menu → Mega menu width),full,container,autoorfixed(fixed_width). Full and container panels line up with the header row: the item and the menu's builder wrappers stop being positioned (src/scss/_megamenu.scss). Fixed and auto panels hang from the item and flip at the screen edge. Header row colors are reset inside the panel (--bt-root-heading,--bt-root-menu-link). - Side panels: a Burger Menu element's "Opens" setting picks the mobile menu or a Side panel template.
Offcanvas::request()queues the template, and the panel is printed once, inwp_footer, as a Bootstrap offcanvas withside(start or end) andwidth(meta_bluetheme_offcanvas). - Late styles: mega menu and side panel templates are queued while the page body is rendered, so their CSS loads in the footer. The panels are closed until opened.
- Settings: each template type's own settings travel through the template settings REST route under the type's key (
Rest::BEHAVIOR), and the builder's Template settings tab shows them, with a save button, for types without display conditions too. - Starters: new mega menu templates start with three link columns and a call-to-action box. New side panels start with the logo, a vertical menu, contacts and social icons.
Popups
- Which popups print: every published popup whose display conditions match the page (
Popups::matching(), all of them, not just the best match), plus any popup a Popup Trigger element asks for (Popups::request()). They print inwp_footeras<dialog class="bt-popup">, never in the live builder canvas or on a template's own address. Their styles load in the head; requested popups load theirs late. - Settings: stored in
_bluetheme_popupand documented inTemplates\Popups. They cover the trigger (load, delay, scroll, exit intent, click only), the frequency (always, once per visit, every N days, once), position, animation, width, the overlay (modal or not), its color, closing on an outside click, the close button, and devices it never opens on by itself. They are edited in the builder's Template settings tab. - Script:
src/frontend/popups.js(its own bundle, loaded only on pages with popups) opens and closes the dialogs.- Modal popups use
showModal(), so focus stays inside. Popups without an overlay useshow(), so the page stays usable. - Frequency is kept in
sessionStorageorlocalStorage, not in cookies. - Popups open from
[data-bt-popup-open="bt-popup-ID"]and from links to#bt-popup-ID. They close from[data-bt-popup-close], from links to#bt-popup-close, and with Escape.
- Modal popups use
- Popup Trigger element: a button that opens a chosen popup (and makes it print on that page), or closes the popup it is in.
- Canvas: CSS that template settings add through
bluetheme_builder_post_cssis now printed in the live builder canvas too, for example the popup width and the sticky header colors.
Single, archive and 404 templates
- Where they apply: Single post, Single project, Blog archive (the blog page, categories, tags, authors and dates) and 404 templates replace the theme layout where their display conditions match (
Templates\Dynamic). Thetemplate_includefilter switches totemplate-dynamic.php, which prints a single template once per post in the loop, and the others once. - Elements: the "Post & archive" category has Post Title, Post Content, Post Excerpt, Featured Image, Post Meta, Post Terms, Author Box, Post Navigation, Related Posts, Comments, Share Buttons, Breadcrumbs, Archive Posts, Archive Description, Project Media and Project Details. They read the post in context or the main query, reuse the theme's own parts (author box, related posts, share, breadcrumbs, portfolio details), and show a builder hint when there is nothing to show.
- Preview in the builder: while a template is edited,
Dynamic::enter()swaps in a sample: the "Preview with" post (or the latest post of the type) for single templates, and the latest posts as the main query for archives. The canvas page (single-bt_template.php) and the builder's REST renders, through thebluetheme_builder_setup_postaction, both use it. - Settings: stored in
_bluetheme_dynamic.previewis the sample post, andsubheaderkeeps the theme's page title bar, which is hidden above these templates by default. - Empty elements: an element whose render prints nothing now leaves no wrapper on the site. The builder keeps the wrapper so the element can still be selected.
- Starters: each type starts with a full layout. Single post: categories, title, meta, image, content, tags, share, author, navigation, related posts, comments. Single project: media beside a sticky details column. Blog archive: title bar and post grid. 404: a large 404, title, search and home button.
- Shop: Single product and Shop archive templates work the same way; see woocommerce.md. The
bluetheme_dynamic_beforeandbluetheme_dynamic_afteractions (template type as argument) run around each template, intemplate-dynamic.phpand on the canvas page; the shop uses them to wrap a product in itsdiv.product.