Skip to main content
Skip to main content

Developers Doc

Template Override

8 mins read 94 Views 3+

The Havenlytics template override system lets you replace any bundled template from your own theme without touching a single plugin file. It uses the same theme-first resolution model as WooCommerce: for each candidate template, Havenlytics looks in your theme first and falls back to the plugin only if the theme has nothing to offer. Master the lookup order below and you can restyle a price field, redesign a single-property layout, or reskin the agent portal — all in an upgrade-safe way. This is the definitive override guide within the Havenlytics Developer Documentation.

Warning: Never edit files inside the plugin’s templates/ directory. Every plugin update overwrites them and your changes are lost. The override system exists precisely so you never have to.

The exact lookup order

When the loader resolves a template name (via hvnly_locate_template() / the loader’s locate_template()), it checks these locations in order and returns the first file that exists:

  1. {theme}/havenlytics/{template_path} — the child theme first, then the parent theme.
  2. {theme}/{template_path} — the theme root, child then parent (a legacy/flat fallback).
  3. {plugin}/templates/{template_path} — the plugin’s bundled default.

The theme override folder name is havenlytics/. It comes from the loader’s $template_path property and is filterable with hvnly_template_path. The relative sub-path inside that folder must mirror the plugin’s templates/ tree exactly — same folders, same filename.

Note: Because the child theme is checked before the parent, a child theme can override a template the parent theme already customized. This is the standard, recommended place to put your overrides.

How to override a template: the rule

Take the plugin-relative path of the template you want to change, and recreate it under havenlytics/ in your theme. That’s it. The tree below the override folder is identical to the plugin’s templates/ tree.

Warning: Mirror the tree exactly. If the plugin file is templates/archive/fields/price.php, the override must be havenlytics/archive/fields/price.php — not havenlytics/price.php and not havenlytics/fields/price.php. A mismatched path is silently ignored and the plugin default keeps rendering.

Worked example 1: override an archive price field

The property archive renders each card’s price through a field partial. To restyle it:

RolePath
Plugin defaulttemplates/archive/fields/price.php
Your theme overridewp-content/themes/your-theme/havenlytics/archive/fields/price.php

Inside the override you have the archive card variables available (loaded through extract($args)): $property_id$property_data$mode, and in preset mode $field. A minimal override:

<?php
/**
 * Theme override: havenlytics/archive/fields/price.php
 * Available: $property_id, $property_data, $mode, $field
 */
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

$price = isset( $property_data['price'] ) ? $property_data['price'] : '';
if ( '' === $price ) {
    return;
}
?>
<div class="my-card-price">
    <span class="my-card-price__amount"><?php echo esc_html( $price ); ?></span>
</div>

Worked example 2: override a single-property preset field

Single-property field templates resolve as single-property/fields/{category}/{template_name}.php, where {category} is defaultpreset, or group. To override the bedrooms preset field on the single property page:

RolePath
Plugin defaulttemplates/single-property/fields/preset/bedrooms.php
Your theme overridewp-content/themes/your-theme/havenlytics/single-property/fields/preset/bedrooms.php

Here the available variables come from PropertySingleRenderer::render_field()$field$value/$field_value$property_id$field_name (the meta key), $field_label$field_type$category, and $field_options.

<?php
/**
 * Theme override: havenlytics/single-property/fields/preset/bedrooms.php
 * Available: $field, $value, $property_id, $field_name, $field_label, $field_type, $category
 */
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

if ( '' === $value || null === $value ) {
    return;
}
?>
<li class="my-feature my-feature--beds">
    <span class="my-feature__label"><?php echo esc_html( $field_label ); ?></span>
    <span class="my-feature__value"><?php echo esc_html( $value ); ?></span>
</li>

Tip: If you want to override a field for every property regardless of category, remember the resolution falls back from {category}/ to a legacy flat map to a generic fallback-field. Copy the specific {category}/{name}.php file to guarantee your version wins.

Worked example 3: override an agent-portal (Workspace) template

The Agent Workspace resolves its templates under a dedicated sub-directory, WorkspaceConstants::TEMPLATE_SUBDIR = 'agent-portal'. Its override order is: {theme}/havenlytics/agent-portal/{template}.php first, then the plugin default. To reskin the Workspace login screen:

RolePath
Plugin defaulttemplates/agent-portal/login.php
Your theme overridewp-content/themes/your-theme/havenlytics/agent-portal/login.php

Note: Only the allow-listed Workspace basenames resolve — canvasdashboardunavailableloginregisterforgot-passwordreset-password. Overriding any other basename in agent-portal/ has no effect unless you extend the list via hvnly_workspace_allowed_templates.

Resolution filters

Every stage of resolution is filterable, so you can relocate the override folder, change the plugin fallback, or swap the located file outright.

FilterWhat it controls
hvnly_template_pathThe theme override folder name (default havenlytics/).
hvnly_default_pathThe plugin fallback base directory.
hvnly_locate_templateThe located path; receives locatednamestemplate_pathdefault_path.
hvnly_get_templateThe final located file inside hvnly_get_template(), before include.
hvnly_get_template_part_templatesThe candidate list built by hvnly_get_template_part().
hvnly_templates_locationThe plugin’s default templates directory returned by hvnly_templates_location().

The Workspace has a parallel set: hvnly_workspace_template_pathhvnly_workspace_default_pathhvnly_workspace_locate_template, and hvnly_workspace_allowed_templates.

Example — point the override folder at a custom sub-directory of your theme:

<?php
add_filter( 'hvnly_template_path', function ( $path ) {
    // Themes now override from {theme}/my-real-estate/ instead of {theme}/havenlytics/
    return 'my-real-estate/';
} );

Performance: Filters like hvnly_locate_template and hvnly_get_template run on every template lookup, which can be hundreds of times on an archive page. Keep them cheap — no database queries, no filesystem scans — or you will pay for it on high-traffic listings.

Lifecycle actions

Two actions fire around every hvnly_get_template() include, letting you inject markup without replacing the whole template:

  • hvnly_before_template_part — fires immediately before the file is included. Args: $template_name$template_path$located$args.
  • hvnly_after_template_part — fires immediately after. Same args.

hvnly_get_template_part() additionally fires a dynamic hvnly_get_template_part_{$slug} action. The Workspace mirrors these with hvnly_workspace_before_template and hvnly_workspace_after_template.

<?php
// Append a disclaimer after the single-property content part renders.
add_action( 'hvnly_after_template_part', function ( $template_name, $template_path, $located, $args ) {
    if ( 'content-single-property.php' === $template_name ) {
        echo '<p class="listing-disclaimer">' . esc_html__( 'Prices subject to change.', 'your-textdomain' ) . '</p>';
    }
}, 10, 4 );

Tip: Reach for these actions before you copy a whole template. If all you need is to add a badge or a disclaimer, an action keeps you fully upgrade-safe and avoids maintaining a forked template.

Common templates and their override paths

PurposePlugin defaultTheme override
Single property entrytemplates/single-property.phphavenlytics/single-property.php
Single property bodytemplates/content-single-property.phphavenlytics/content-single-property.php
Property archive entrytemplates/archive-property.phphavenlytics/archive-property.php
Archive price fieldtemplates/archive/fields/price.phphavenlytics/archive/fields/price.php
Archive looptemplates/archive/property-loop.phphavenlytics/archive/property-loop.php
Single agent entrytemplates/single-agent.phphavenlytics/single-agent.php
Agency taxonomytemplates/taxonomy-hvnly_agent_agency.phphavenlytics/taxonomy-hvnly_agent_agency.php
Single-property preset fieldtemplates/single-property/fields/preset/bedrooms.phphavenlytics/single-property/fields/preset/bedrooms.php
Search filter sidebartemplates/search/filter-sidebar.phphavenlytics/search/filter-sidebar.php
Map containertemplates/map/map-container.phphavenlytics/map/map-container.php
Workspace logintemplates/agent-portal/login.phphavenlytics/agent-portal/login.php

Best practices

  • Do your overrides in a child theme so a parent-theme update never wipes them.
  • Override the narrowest template that solves the problem — a field partial beats a whole entry file.
  • Keep the copied file thin: change only what you must, and preserve the variables the renderer passes in.
  • Prefer a lifecycle action (hvnly_before/after_template_part) over a full copy whenever you are only adding, not replacing, markup.
  • Document each override in a comment header so the next developer knows which plugin file it shadows.

Security notes

Security: Overridden templates receive the same unescaped variables as the originals. Escape on output — esc_html()esc_attr()esc_url()wp_kses_post() — and guard direct access with if ( ! defined( 'ABSPATH' ) ) { exit; }.

Security: When extending the Workspace allow-list via hvnly_workspace_allowed_templates, add only fixed, known basenames. Never derive an allowed name from request data — that reopens the arbitrary-inclusion hole the allow-list exists to close.

Common mistakes

  • Copying a template to {theme}/havenlytics/price.php instead of mirroring the full sub-path {theme}/havenlytics/archive/fields/price.php.
  • Editing plugin templates/ files directly — lost on the next update.
  • Putting overrides in the parent theme, then losing them when the parent updates. Use a child theme.
  • Overriding a non-allow-listed Workspace basename and wondering why nothing changes.
  • Re-declaring or unsetting variables the renderer provides, breaking downstream markup.
  • Adding expensive logic in hvnly_locate_template, slowing every card on an archive page.

Need more help?

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