The Havenlytics Builder System is made of exactly two drag-and-drop builders, each backed by its own PHP class, REST endpoints, and WordPress option. The first controls how a property listing card is laid out on archives; the second controls the Add Property form in the admin. This guide documents both from a developer’s perspective — their storage shapes, allowed field types, the REST contract, and how the saved card configuration drives render-time output.
Note: There is no separate “Search Builder” or “Archive Builder” class. The archive layout is produced by the Property Card Builder configuration, and the archive query is produced by the search engine. Do not look for a third builder class — it does not exist.
Architecture at a glance
| Builder | Class | Option key | REST base | Purpose |
|---|---|---|---|---|
| Property Card Builder | DnDCardBuilder | hvnly_property_card.sections | hvnlynab/v1/pb-card-builder | Archive/listing card layout |
| Property Form Builder | DnDSections | hvnly_property_builder.sections | hvnlynab/v1/pb-dnd-sections | Admin “Add Property” form |
Both classes live in includes/Api/Type/Builders/, both register routes under the admin REST namespace hvnlynab/v1, and both gate their endpoints behind current_user_can( 'manage_options' ) with write operations re-verifying the wp_rest nonce.
1. Property Card Builder (DnDCardBuilder)
The Property Card Builder defines the visual layout of a property card on archive and search pages — which fields appear, in which section, and in what order. The layout is persisted to a single option, hvnly_property_card.sections, and consumed at render-time by the PropertyCardRenderer.
REST endpoints
| Method & route | Handler | Behaviour |
|---|---|---|
GET /hvnlynab/v1/pb-card-builder | get_all() | Returns saved sections. When empty, returns get_default_sections() (14 sections) with has_defaults = true. |
POST /hvnlynab/v1/pb-card-builder | save_all() | Accepts {sections:[...]} or a bare array; runs build_sections_storage(), then update_option. |
DELETE /hvnlynab/v1/pb-card-builder | delete_all() | Requires confirm=true. Blocked with HTTP 409 when BatchProcessor::count_properties() > 0. |
POST /hvnlynab/v1/pb-card-builder/reset | reset_defaults() | Requires confirm=true. Restores the default sections. |
Security: The destructive
DELETEandresetoperations are guarded byBatchProcessor::count_properties()andhvnly_rest_deletion_confirmed(). If any properties exist, a bare delete returns409— the caller must passconfirm=trueto proceed. Never build integrations that assume the delete will silently succeed.
Storage shape
The option stores an ordered list of sections keyed by id. Each section carries a display mode and an ordered list of fields:
{
"id": "card-title",
"title": "Title",
"icon": "heading",
"mode": "both", // "both" | "preset"
"order": 4,
"fields": [
{
"id": "title",
"type": "title",
"label": "Property Title",
"mode": "both",
"value": [], // array
"order": 0
}
]
}
Allowed card field types (40)
The sanitize_fields() routine silently drops any field whose type is not in this allow-list, so custom types cannot be smuggled into the option:
image, carousel, badge, status, time, favorite, views, title, rating, phone, email, website, address, datetime, price, price-per-sqft, beds, baths, kitchens, floor, rooms, receptions, sqft, gsft, button, view-btn, schedule-tour-button, feature, excerpt, location, schedule-tour, property_type, avatar, tags, garage, comments, url, date, faq-count, repeater-count.
Default sections
When no configuration exists, the builder seeds these sections: image-overlay-top-left, image-overlay-top-right, thumbnail, image-overlay-bottom-left, image-overlay-bottom-right, avatar, card-title, card-content, features, excerpt, location, card-footer-left, and card-footer-right. The ensure_legacy_sections() helper re-injects the avatar section when older saved configurations are missing it, so upgrades never lose the avatar slot.
Note: The constructor exposes the
hvnly_persist_card_builder_defaultsfilter, letting you control whether the seeded defaults are written back to the database. A static helper,DnDCardBuilder::get_default_sections_for_storage(), returns the default set in storage shape for programmatic use.
Reading the saved card configuration in PHP
<?php
/**
* Inspect the current Property Card Builder layout.
* The option stores an ordered list of section arrays.
*/
$config = get_option( 'hvnly_property_card', array() );
$sections = isset( $config['sections'] ) ? $config['sections'] : array();
foreach ( $sections as $section ) {
$field_types = wp_list_pluck( $section['fields'], 'type' );
printf(
"Section %s (%s) has fields: %s\n",
esc_html( $section['id'] ),
esc_html( $section['mode'] ),
esc_html( implode( ', ', $field_types ) )
);
}
2. Property Form Builder (DnDSections)
The Property Form Builder defines the admin “Add Property” form: its tabs, the fields inside each tab, and the group metadata that keeps related fields (address, latitude, longitude, map preview) linked. It persists to hvnly_property_builder.sections.
REST endpoints
hvnlynab/v1/pb-dnd-sections— read and save the tab/field configuration.hvnlynab/v1/master-base-ids— enumerate the master base identifiers in use.hvnlynab/v1/generate-unique-ids— mint collision-free group identifiers.hvnlynab/v1/pb-dnd-sections/reset-unified— reset to the unified default configuration.
Payload shape
{
"tabs": [
{
"id": "basic-info",
"title": "Basic Info",
"icon": "home",
"order": 0,
"collapsed": false,
"required": true,
"fields": [
{
"type": "map",
"group_id": "...",
"group_type": "map",
"group_base_id": "location_1",
"group_name": "Location",
"group_position": 0,
"group_total": 4,
"metaKey": "...",
"address_field_name": "location_1_address",
"lat_field_name": "location_1_latitude",
"lng_field_name": "location_1_longitude",
"map_field_name": "location_1_preview"
}
]
}
]
}
Cross-linked map fields follow the convention {group_base_id}_{address|latitude|longitude|preview}, which is what lets the search engine resolve coordinates later.
Allowed property field types
get_allowed_property_field_types() permits: text, textarea, number, select, checkbox, file, gallery, map, video, property_docs, agents, faq, repeater, and price_label, plus any types contributed by Database::get_field_registry().
The group-identity engine and meta preservation
The most important internal mechanism in the Form Builder is its group-identity engine, which guarantees that renaming or reordering a field never orphans the property meta already stored under it. On every save, save_all():
- Regenerates any colliding identifiers via
generate_unique_base_id()andgenerate_unique_group_id(). - Runs
consolidate_tab_group_fields()to merge fragmented groups. - Deduplicates through
Core\GroupFieldIdentity::deduplicate_sections()andCore\SectionIdentity::deduplicate_equivalent_sections(). - When the configuration changed, builds a meta remap with
Core\DataPreservation\BuilderMetaMigrator::build_remap_between_configs(), schedules the migration viaschedule_property_remap(), and snapshots the prior state through theBackupManager.
Note: Because of
BuilderMetaMigrator, renaming a field in the builder does not orphan existing property meta — the values are remapped to the new key. This is a deliberate data-preservation guarantee. Do not write custom code that hard-codes old field keys expecting them to survive a rename; read through the group metadata instead.
Basic Info locked fields
The builder always guarantees a Basic Info tab via is_basic_info_tab() and ensure_basic_info_fields() (invoked on every save and read). Its get_canonical_basic_info_fields() returns 11 locked meta keys that cannot be removed:
_hvnly_property_price (a price_label field), _reception_rooms, _bedrooms, _bathrooms, _half_bathrooms, _kitchens, _total_rooms, _floors, _year_built, _mls_number, and _garage_sqft.
The default configuration itself is sourced from Core\UnifiedFieldGenerator::get_unified_configuration(), falling back to DefaultTabSectionsData::get_default_sections_for_dnd().
How the saved card config drives render-time output
Saving a card layout is only half the story — the PropertyCardRenderer reads it back at render-time. The entry point is hvnly_render_property_card( $id ) (in template-functions.php), which delegates to hvnly_get_property_card_renderer()->render_property_card_dynamic( $id, $data ).
The renderer (includes/Frontend/PropertyCardRenderer.php) loads hvnly_property_card.sections once per request via load_sections_from_storage(), then branches:
- Preset path — when configured sections with fields exist, it calls
render_preset_sections_structure(). - Default path — otherwise it calls
render_default_fields_structure().
The preset path renders the thumbnail first (collecting every image-overlay-* section as overlays on that thumbnail), then emits a fixed section order: card-title → header, then excerpt, features, card-content → content, and finally card-footer-left → footer. Footers are combined through render_combined_footer_section().
Note: The visual order on the card is fixed by the renderer, not purely by the saved
ordervalues within arbitrary sections. Overlays are always painted onto the thumbnail; the header/content/footer regions always follow the same sequence. Plan card designs around this fixed skeleton.
Each field’s type is mapped to a template slug through hvnly_get_field_template_mapping() (filterable), after which hvnly_render_field() loads templates/archive/fields/{slug}.php or templates/archive/sections/{name}.php via hvnly_get_template().
Extension filters
| Filter | What it changes |
|---|---|
hvnly_section_template_mapping | Remap a section id to a different section template. |
hvnly_field_template_mapping | Remap a field type to a different field template slug. |
hvnly_persist_card_builder_defaults | Control whether seeded card defaults are written back to the option. |
Example: remap a field template
<?php
/**
* Render the "price" field through a custom template instead of the
* built-in archive/fields/price.php partial.
*
* Place your override at:
* your-theme/havenlytics/archive/fields/price-highlight.php
*/
add_filter( 'hvnly_field_template_mapping', function ( array $map ) {
$map['price'] = 'price-highlight';
return $map;
} );
Best practices
- Read the layout through
get_option( 'hvnly_property_card' )rather than reconstructing it — the option is the single source of truth. - Never write unknown field types directly into the option;
sanitize_fields()will drop them and you will lose data silently. - To change how a field looks, override its template (theme
havenlytics/folder) or usehvnly_field_template_mapping— do not fork the renderer. - When integrating with the Form Builder, always resolve meta through the group metadata (
group_base_id+metaKey), never hard-coded keys, soBuilderMetaMigratorrenames don’t break you.
Performance notes
PropertyCardRendererloads the sections option once per request; it is not re-read per card, so rendering a full archive is a single option lookup plus per-card template includes.- Template lookups honour
hvnly_get_template(), which caches path resolution — keep custom overrides in the themehavenlytics/folder rather than filtering paths on every call.
Common mistakes
- Assuming a third “Search/Archive builder” exists — the archive card is the Card Builder output; the archive results are the search engine’s output.
- Calling
DELETE /pb-card-builderwithoutconfirm=truewhen properties exist and treating the409as an error rather than the safety guard it is. - Expecting the saved section
orderto override the renderer’s fixed header/content/footer skeleton. - Hard-coding Form Builder meta keys and losing data after a field rename remaps them.
Warning: The file
_builder_option.txtat the repository root is not documentation — it is straymysql.exe --helpoutput left as a debug artifact. Do not treat it as a specification of the builder option format.