Cookbook

Cookbook: Extend Display Rules

2 min read

Notifal Lite ships page, post, product, post type, and WooCommerce cart display rules. Notifal Pro adds categories, URL conditions, and user targeting by hooking into extension points in the base plugin.

Follow the same two-layer pattern Pro uses:

  1. Admin UI , render fields on notifal_display_rules_* section actions (see display-rules-settings.php).
  2. Runtime logic , register ONPAGE_DISPLAY_RULES_* filters (see DisplayRulesService and ProDisplayRulesSecurityService).

Hook references:

Section action hooks (admin UI)

The notification editor Display Rules tab calls these actions inside the rule builder modal:

ActionUsed by Pro for
notifal_display_rules_categories_sectionCategory archive targeting
notifal_display_rules_url_match_sectionURL pattern conditions
notifal_display_rules_users_sectionLogin status and visit history

Pro wires them in ProDisplayRulesIntegrationService::register():

add_action( 'notifal_display_rules_categories_section', [ $renderer, 'renderCategoriesSection' ], 10 );
add_action( 'notifal_display_rules_url_match_section', [ $renderer, 'renderUrlMatchSection' ], 10 );
add_action( 'notifal_display_rules_users_section', [ $renderer, 'renderUsersSection' ], 10 );

Render markup inside a .notifal-display-condition-section container so the existing Display Rules JavaScript can show and hide sections by rule type.

Filter hooks (data and evaluation)

Constants live in FilterHooks:

ConstantHook slugPurpose
ONPAGE_DISPLAY_RULES_SUPPORTED_TYPESnotifal/onpage/display_rules/supported_typesRegister rule type metadata for the admin dropdown
ONPAGE_DISPLAY_RULES_BEFORE_VALIDATIONnotifal/onpage/display_rules/before_validationAdjust rules before save or evaluation
ONPAGE_DISPLAY_RULES_EVALUATION_RESULTnotifal/onpage/display_rules/evaluation_resultOverride or extend server-side rule matching
ONPAGE_DISPLAY_RULES_SANITIZED_SETTINGSnotifal/onpage/display_rules/sanitized_settingsModify sanitized rules after save processing

DisplayRulesService::shouldDisplay() applies BEFORE_VALIDATION, then short-circuits when EVALUATION_RESULT returns a non-null boolean.

Pro registers all three evaluation-related filters in ProDisplayRulesSecurityService::registerHooks().

Example: custom "referrer" rule type

This minimal integration adds a new rule type referrer, admin fields on a dedicated section hook, and server-side evaluation.

1. Bootstrap class

<?php
/**
 * Plugin Name: My Notifal Referrer Display Rule
 * Description: Adds a referrer-based display rule to Notifal notifications.
 * Version: 1.0.0
 * Requires PHP: 7.4
 */

defined( 'ABSPATH' ) || exit;

use MyNotifalReferrerRule\ReferrerDisplayRuleIntegration;

/**
 * Boot integration after Notifal initializes.
 *
 * @return void
 */
add_action( 'notifal/initialized', function (): void {
    // Instantiate and register hooks only when Notifal core is ready.
    $integration = new ReferrerDisplayRuleIntegration();
    $integration->register();
} );

2. Integration service

<?php

namespace MyNotifalReferrerRule;

defined( 'ABSPATH' ) || exit;

use Notifal\Infrastructure\WordPress\Hooks\FilterHooks;
use Notifal\Modules\OnPageNotification\Application\Services\Settings\DisplayRulesDataNormalizer;

/**
 * Registers admin UI and evaluation hooks for the referrer display rule.
 */
class ReferrerDisplayRuleIntegration {

    /**
     * Rule type slug stored in _notifal_display_rules_data.
     */
    private const RULE_TYPE = 'referrer';

    /**
     * Register WordPress hooks.
     *
     * @return void
     */
    public function register(): void {
        // Render admin fields when the referrer rule type is selected.
        add_action( 'notifal_display_rules_url_match_section', [ $this, 'renderReferrerSection' ], 20 );

        // Expose the rule type in the admin rule type list (Pro-active sites use the same filter).
        add_filter( FilterHooks::ONPAGE_DISPLAY_RULES_SUPPORTED_TYPES, [ $this, 'addRuleType' ] );

        // Evaluate referrer rules on the server during eligibility checks.
        add_filter( FilterHooks::ONPAGE_DISPLAY_RULES_EVALUATION_RESULT, [ $this, 'evaluateReferrerRules' ], 10, 4 );
    }

    /**
     * Add referrer rule metadata for the Display Rules UI.
     *
     * @param array<string, array<string, mixed>> $rule_types Existing rule types.
     * @return array<string, array<string, mixed>>
     */
    public function addRuleType( array $rule_types ): array {
        // Append a custom rule definition (icon and label appear in the admin builder).
        $rule_types[ self::RULE_TYPE ] = [
            'label' => __( 'Referrer', 'my-notifal-referrer-rule' ),
            'icon'  => '🔗',
        ];

        return $rule_types;
    }

    /**
     * Output admin fields for the referrer rule (placed near URL conditions).
     *
     * @return void
     */
    public function renderReferrerSection(): void {
        ?>
        <div class="notifal-display-condition-section notifal-display-referrer notifal-hidden">
            <label for="target_referrer_contains">
                <?php esc_html_e( 'Referrer contains', 'my-notifal-referrer-rule' ); ?>
            </label>
            <input
                type="text"
                name="target_referrer_contains"
                id="target_referrer_contains"
                class="regular-text"
                value=""
                placeholder="<?php esc_attr_e( 'google.com', 'my-notifal-referrer-rule' ); ?>"
            />
            <p class="description">
                <?php esc_html_e( 'Show the notification when the HTTP referrer contains this string.', 'my-notifal-referrer-rule' ); ?>
            </p>
        </div>
        <?php
    }

    /**
     * Evaluate referrer rules when present in the saved rule set.
     *
     * @param bool|null              $result            Existing result (null = continue default logic).
     * @param array<string, mixed>   $rules             Saved display rules container.
     * @param string                 $combination_logic AND or OR combination mode.
     * @param array<string, mixed>   $context           Evaluation context from DisplayRulesService.
     * @return bool|null
     */
    public function evaluateReferrerRules( $result, array $rules, string $combination_logic, array $context ) {
        // Respect an earlier filter that already decided visibility.
        if ( $result !== null ) {
            return $result;
        }

        // Extract individual rule items from the normalized container shape.
        $items = DisplayRulesDataNormalizer::extractItems( $rules );

        // Track whether any referrer rule exists in this notification.
        $has_referrer_rule = false;

        // Collect boolean match results keyed by rule id.
        $matches = [];

        foreach ( $items as $item ) {
            // Read the rule type slug from the saved item.
            $type = isset( $item['type'] ) ? sanitize_key( (string) $item['type'] ) : '';

            // Skip items that are not referrer rules.
            if ( $type !== self::RULE_TYPE ) {
                continue;
            }

            $has_referrer_rule = true;

            // Read rule-specific data saved from the admin UI.
            $data = isset( $item['data'] ) && is_array( $item['data'] ) ? $item['data'] : [];
            $needle = isset( $data['referrer_contains'] ) ? sanitize_text_field( (string) $data['referrer_contains'] ) : '';

            // Read the visitor referrer from context or the current request.
            $referrer = isset( $context['http_referrer'] )
                ? sanitize_text_field( (string) $context['http_referrer'] )
                : ( isset( $_SERVER['HTTP_REFERER'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_REFERER'] ) ) : '' );

            // Empty needle matches all referrers (same as "any").
            $matched = ( $needle === '' ) || ( $referrer !== '' && stripos( $referrer, $needle ) !== false );

            $rule_id = isset( $item['id'] ) ? sanitize_key( (string) $item['id'] ) : self::RULE_TYPE;
            $matches[ $rule_id ] = $matched;
        }

        // Defer to core logic when this notification has no referrer rules.
        if ( ! $has_referrer_rule ) {
            return null;
        }

        // Combine per-rule results using AND or OR semantics.
        if ( $combination_logic === 'AND' ) {
            $combined = ! in_array( false, $matches, true );
        } else {
            $combined = in_array( true, $matches, true );
        }

        // Honor show_if vs hide_if visibility mode when provided in context.
        $visibility = isset( $context['visibilityMode'] )
            ? DisplayRulesDataNormalizer::sanitizeVisibilityMode( (string) $context['visibilityMode'] )
            : DisplayRulesDataNormalizer::VISIBILITY_SHOW_IF;

        if ( $visibility === DisplayRulesDataNormalizer::VISIBILITY_HIDE_IF ) {
            return ! $combined;
        }

        return $combined;
    }
}

Wire your admin JavaScript to map target_referrer_contains into rule data.referrer_contains with type referrer when saving, following the same shape Pro uses (id, type, data).

Sanitize custom rule data on save

Hook notifal/onpage/display_rules/sanitized_settings to normalize custom keys before they are stored in _notifal_display_rules_data:

<?php

defined( 'ABSPATH' ) || exit;

use Notifal\Infrastructure\WordPress\Hooks\FilterHooks;
use Notifal\Modules\OnPageNotification\Application\Services\Settings\DisplayRulesDataNormalizer;

add_filter(
    FilterHooks::ONPAGE_DISPLAY_RULES_SANITIZED_SETTINGS,
    function ( array $sanitized, array $settings ): array {
        // Walk each saved rule item after core sanitization.
        $items = DisplayRulesDataNormalizer::extractItems( $sanitized );

        foreach ( $items as $index => $item ) {
            // Only touch referrer rule items.
            if ( ( $item['type'] ?? '' ) !== 'referrer' ) {
                continue;
            }

            // Ensure data is an array before writing keys.
            if ( ! isset( $items[ $index ]['data'] ) || ! is_array( $items[ $index ]['data'] ) ) {
                $items[ $index ]['data'] = [];
            }

            // Sanitize the referrer substring stored in post meta.
            $raw = $items[ $index ]['data']['referrer_contains'] ?? '';
            $items[ $index ]['data']['referrer_contains'] = sanitize_text_field( (string) $raw );
        }

        // Re-wrap items into the container shape Notifal expects.
        return DisplayRulesDataNormalizer::wrapItems( $items );
    },
    10,
    2
);

How Notifal Pro maps to these hooks

Pro classResponsibility
ProDisplayRulesIntegrationServiceRegisters the three notifal_display_rules_* section renderers
ProDisplayRulesRendererServiceOutputs category, URL, and user admin fields
ProDisplayRulesSecurityServiceAdds pro rule types, validates access, evaluates pro rules

Pro adds rule types in addProRuleTypes() via ONPAGE_DISPLAY_RULES_SUPPORTED_TYPES. Evaluation for Pro rule types runs in evaluateProRules().

When extending display rules in your own plugin, return null from EVALUATION_RESULT when your rule type is not present so core Lite logic continues.

Client-side rules

Some rules (users visit history, cart totals, page targeting) are mirrored to the frontend via client_user_rules, client_cart_rules, and client_page_rules in NotificationDataPreparer. Server-only rules (like referrer on first paint) should be evaluated in NotificationEligibilityChecker through the filters above, or documented as client-side if you add matching frontend logic.

Cookbook series

  1. Cookbook: Modify Notification Before Display
  2. Cookbook: Add Custom Dynamic Tag
  3. Cookbook: Customize Template Import
  4. Cookbook: Extend Display Rules (you are here)
  5. Cookbook: Register a Custom HTML Builder Widget

Hook articles