Skip to main content
Skip to main content

Developers Doc

Template Loader

11 mins read 5 Views 3+

The Havenlytics template loader is the layer that decides which PHP template renders a property, an agent, an archive, or an individual field. It follows the same battle-tested pattern used by WooCommerce and Easy Digital Downloads: a small set of loader classes intercept WordPress rendering, a resolver walks a theme-first lookup order, and a family of helper functions include the located file with a controlled set of variables. This guide, part of the Havenlytics Developer Documentation, explains every loader, every helper function, how page context maps to top-level templates, and exactly which variables each template receives.

Note: This page covers how templates are found and loaded. For the theme-first override hierarchy and worked examples of copying templates into your theme, see Template Override System.

The three loaders

Havenlytics ships three distinct loaders. Two operate on the WordPress front end; the third is on-demand and used only by the Agent Workspace.

1. Frontend\TemplateLoader (template_include)

HvnlyNab\Frontend\TemplateLoader (includes/Frontend/TemplateLoader.php) is the primary loader. It is registered as a service in includes/Frontend.php and hooks WordPress at the standard integration points:

  • add_filter('template_include', [$this, 'template_loader']) at default priority 10 — the entry point that swaps in a Havenlytics template.
  • comments_template — routes comment templates for the plugin post types.
  • generate_rewrite_rules — registers the author/{slug}/properties endpoint.
  • body_class / admin_body_class — add hvnly-* classes for styling hooks.
  • add_filter('hvnly_template_path', ...) — supplies the theme override folder name.

Its key properties define the contract for the whole system: $post_type = 'hvnly_property'$slug = 'property'$template_path = 'havenlytics/' (the theme override folder), and $default_path = HVNLYNAB_PATH . 'templates/' (the plugin’s bundled templates).

2. Frontend\EnhancedTemplateLoader

HvnlyNab\Frontend\EnhancedTemplateLoader (includes/Frontend/EnhancedTemplateLoader.php) extends TemplateLoader and is the loader actually preferred at runtime — hvnly_get_template_loader() and Frontend::get_template_loader() both return it. It adds an hvnly_locate_template filter hook, but its per-field method locate_field_template() is effectively a no-op: real field-template resolution now lives in PropertySingleRenderer (single property) and PropertyCardRenderer (archive cards).

Note: EnhancedTemplateLoader::get_field_template() is deprecated as of 2.3.0. Do not build new integrations on it — resolve fields through the renderers or copy the field template into your theme instead.

3. Workspace\WorkspaceTemplateLoader (on-demand, allow-listed)

HvnlyNab\Workspace\WorkspaceTemplateLoader (includes/Workspace/WorkspaceTemplateLoader.php) is instantiated in WorkspaceBootstrap.php. Unlike the front-end loaders it does not hook template_include — in the current phase the Workspace is API-driven and its templates are loaded explicitly via ->load($template, $args). For safety it enforces an allow-list of basenames and rejects anything else through sanitize_template_name():

  • canvas
  • dashboard
  • unavailable
  • login
  • register
  • forgot-password
  • reset-password

Security: The Workspace loader’s allow-list is a deliberate guard against arbitrary file inclusion. Any template name not on the list is refused, so never assume you can load a custom basename through it — extend the allow-list via hvnly_workspace_allowed_templates if you genuinely need to.

Bootstrap fallback

includes/Functions/template-bootstrap.php defines hvnly_load_template_functions(), which requires all of the Functions/*-functions.php files. It is hooked on both plugins_loaded (priority 15) and template_include (priority 1), so template helpers are always available. If the full loader has not yet initialized, it defines minimal fallback versions of hvnly_get_template_part() and hvnly_get_template() so template calls never fatal.

Template helper functions

These live in includes/Functions/template-functions.php and are the public API you should call from themes, shortcodes, and integrations.

FunctionPurpose
hvnly_locate_template($names, $path, $default)Delegates to the loader’s locate_template() resolver and returns the first matching absolute file path following the override order.
hvnly_get_template($name, $args, $path, $default)The workhorse. Calls extract($args), ensures a .php extension, locates the file, applies the hvnly_get_template filter, fires hvnly_before_template_part / hvnly_after_template_part, and includes the file.
hvnly_get_template_html($name, $args, $path, $default)Output-buffered variant of hvnly_get_template() — returns the rendered HTML as a string instead of echoing it.
hvnly_get_template_part($slug, $name, $args)Builds the candidate list ["{slug}-{name}.php", "{slug}.php"], fires hvnly_get_template_part_{$slug}, applies hvnly_get_template_part_templates, and loads via load_template().
hvnly_templates_location()Returns apply_filters('hvnly_templates_location', HVNLYNAB_PATH . 'templates/') — the plugin’s default templates directory.

Tip: Use hvnly_get_template() when you want to echo directly into the page, and hvnly_get_template_html() when you need the markup as a string — for example to return it from a shortcode or embed it in a REST response.

How template_loader() maps page context to top-level templates

The template_loader($template) method on Frontend\TemplateLoader is registered on template_include. It inspects the current WordPress query with the standard conditional tags, picks a top-level template filename, then hands that filename to locate_template() to resolve the actual file. The mapping is:

Page context (conditional)Top-level template
is_singular('hvnly_property')single-property.php
is_singular(AgentConstants::POST_TYPE) (hvnly_agent)single-agent.php
is_post_type_archive(AgentConstants::POST_TYPE)archive-agent.php
is_tax(AgentConstants::TAXONOMY_AGENCY) (hvnly_agent_agency)taxonomy-hvnly_agent_agency.php
Property archivearchive-property.php

If none of the conditions match, the original theme template flows through unchanged. This is why a Havenlytics install never breaks unrelated pages — the loader only claims the property, agent, agency, and archive contexts.

Note: The top-level entry templates run inside the standard WordPress loop (have_posts() / the_post()) and delegate the visible markup to a content part, e.g. hvnly_get_template_part('content', 'single-property'). Override the content part, not the entry file, when you only want to change card or body markup.

Variables passed into templates via extract($args)

When a template is loaded, its $args array is run through extract(), so each key becomes a local variable inside the template. The exact set depends on which renderer loaded the template.

Archive field templates

Templates under templates/archive/fields/*.php are loaded by hvnly_render_field() and the PropertyCardRenderer::render_* methods. They receive:

  • $property_id — the property post ID.
  • $property_data — the normalized array from hvnly_get_property_data() (keys include idtitlepermalinkexcerptpriceaddressphonebedroomsbathroomsareagarageis_featuredauthor_idfeatured_imagegallery_image_idsgallery_orderlocationscategoriesauthor_posts_urlcurrent_filtersview_type).
  • $mode — either 'preset' or 'default'.
  • $field — in preset mode, the field configuration array.

Archive section templates additionally receive $all_sections and $section; the header section also gets $show_avatar.

Single-property field templates

Templates under single-property/fields/** are loaded by PropertySingleRenderer::render_field() from the $field_data it prepares. They receive:

  • $field — the field configuration.
  • $value / $field_value — the stored field value.
  • $property_id — the property post ID.
  • $field_name — the underlying meta key.
  • $field_label — the display label.
  • $field_type — the field type slug.
  • $category — one of defaultpreset, or group.
  • $field_options — options for select/choice fields.

The path resolves as single-property/fields/{category}/{template_name}.php; if that is missing it falls back to a legacy flat map and finally to a generic fallback-field template. Group fields load single-property/fields/group/{group_type}.php and receive $group_data.

Common mistake: Assuming archive templates and single-property templates share the same variables. They do not — archive cards get $property_data (a flat array), while single-property field templates get $field_name$field_type$category, and a scalar $value. Always check which surface you are overriding.

Workspace templates

Workspace templates receive whatever $args are passed to ->load(), extracted with EXTR_SKIP so existing variables are never clobbered.

Annotated templates/ directory map

Every path below is relative to HVNLYNAB_PATH . 'templates/'. Mirror this exact tree inside your theme’s havenlytics/ folder to override any file.

  • Top-level entries — single-property.phpsingle-agent.phparchive-property.phparchive-agent.phptaxonomy-hvnly_agent_agency.php.
  • Loop card variants — content-single-property.phpcontent-single-agent.phpcontent-single-agency.phpcontent-archive-*.phpcontent-property*.php.
  • archive/ — property-loop.phpview-controls.phpsection-title.phpfields/ (per-field card partials: price, beds, baths, sqft, address, title, thumbnail, status, features, badge, rating, favorite, phone, email, website, location, views, view-btn, schedule-tour, excerpt, footer, avatar, date/time/datetime, floor, kitchens, rooms, receptions, gsft, garage-sqft, faq-count, repeater-count); sections/ (header, thumbnail, excerpt, features, footer, content, section).
  • single-property/ — gallery-carousel, gallery-card, summary-card, description-card, location-card, video-card, faq-card, repeater-card, stats, sidebar, breadcrumb, actions, meta, title-section, svg-icons, similar-carousel-*, modal-contact-agent, modal-gallery, modal-video; fields/ (default/: text, textarea, number, select, checkbox, checkbox-repeater, date, url; preset/: bedrooms, email, phone, website, checkbox-repeater; group/: agents-section, property_docs; plus *-field.php legacy flat + fallback); partials/ (property-agent-card, -sidebar, -sidebar-form, -sidebar-footer, -section-card, -section-form, -widget, contact-agent-button, contact-agent-form).
  • agent-portal/ — canvas, dashboard, login, register, forgot-password, reset-password, unavailable, index (Workspace allow-listed).
  • single-agent/single-agency/property-archive/partials/partials/.
  • search/ — filter-sidebar, search-filters, pagination, ajaxload-more, result-count, loaded-counter, loop-start, loop-end.
  • map/ — map-container, map-error, custom-marker, property-popup.
  • global/ — wrapper-start, wrapper-end.
  • shortcodes/ — property-grid.php, property-list.php.
  • widgets/ — agent-listings-carousel.php.
  • ajax/ — error-message.php.
  • emails/ and partials/ — property-import-success, workspace-* notices.
  • contact-agent/emails/ and partials/ — admin-notification, admin-reply, agent-notification, sender-autoreply.

Code example: loading a template with hvnly_get_template()

The signature is hvnly_get_template($template_name, $args = array(), $template_path = '', $default_path = ''). Pass an $args array and every key is available as a variable inside the template file. The example below renders a property summary card partial from a custom shortcode:

<?php
add_shortcode( 'my_property_summary', function ( $atts ) {
    $atts = shortcode_atts( array( 'id' => get_the_ID() ), $atts );
    $property_id = absint( $atts['id'] );

    // Build the same data array the loop uses.
    $property_data = hvnly_get_property_data( $property_id );

    // Return the markup as a string (buffered variant) for a shortcode.
    return hvnly_get_template_html(
        'single-property/summary-card.php',
        array(
            'property_id'   => $property_id,
            'property_data' => $property_data,
        )
    );
} );

Because hvnly_get_template() resolves through the override hierarchy, a theme that ships havenlytics/single-property/summary-card.php will automatically supply its own version — no change to the shortcode needed.

Performance: Prefer hvnly_get_template_html() only when you truly need a string; the output buffer has a small cost. For direct rendering inside a template, call hvnly_get_template() so output streams straight to the browser.

Best practices

  • Override the smallest unit that solves the problem — a field or content part, not a top-level entry file.
  • Always pass data through $args; never rely on undeclared globals inside a template.
  • Escape late, in the template itself, using esc_html()esc_attr(), and esc_url().
  • Use hvnly_templates_location() rather than hardcoding HVNLYNAB_PATH . 'templates/', so a filter can relocate the base directory.

Security notes

Security: Every value in $property_data originates from post meta and user input. Treat it as untrusted and escape on output. The loader includes files but performs no escaping for you.

Security: The Workspace loader validates names against a fixed allow-list before including any file — mirror that discipline if you build your own on-demand loader; never pass a raw request value into an include.

Common mistakes

  • Hooking template_include at priority 10 in a theme and unintentionally overriding the Havenlytics loader — use a later priority if you must post-process.
  • Editing the plugin’s templates/ files directly; upgrades overwrite them. Copy into the theme instead.
  • Building new field integrations on the deprecated get_field_template() method.
  • Expecting the Workspace loader to run on template_include — it is on-demand only.

Need more help?

Can't find what you're looking for? Our team and community are here to help you ship faster.