Function & Hook Reference
Function Organization
Plugin Configuration & Lifecycle
Core (includes/core/functions.php)
Public API + plugin lifecycle functions. Does not contain internal utilities — those live in helpers.php.
htax_health_check()- Plugin health diagnosticshtax_load_textdomain()- Load translationshtax_add_settings_link()- Add settings link to plugins pagehtax_add_plugin_meta_links()- Add meta links to plugin rowhtax_check_version()- Version comparison for upgradeshtax_upgrade()- Run upgrade routineshtax_is_taxonomy_enabled()- Check if taxonomy is enabled in settings AND currently registered viataxonomy_exists(); returns false for unregistered taxonomies even if the setting says enabled, suspending HyperTaxonomy features without losing stored datahtax_get_available_taxonomies()- Get all public taxonomies usable with HyperTaxonomyhtax_get_taxonomy_info()- Get taxonomy object with metadatahtax_get_enabled_taxonomies()- Get list of enabled taxonomy slugs; filters out any slugs that are no longer registered (taxonomy_exists()check), so callers always receive a clean list safe to pass to WordPress taxonomy functionshtax_get_indexed_post_types()- Get post types included in the search index (default:['post', 'page'])htax_get_taxonomy_features()- Get feature configuration for a taxonomyhtax_taxonomy_has_feature()- Check if specific feature is enabled for taxonomyhtax_get_cache_duration($type)- Get cache duration from settings; types:search(default 30 min),autocomplete(default 5 min),relationship(default 0 = never expire, explicit invalidation),sticky(default 0),shortcode(default 0)htax_generate_transient_key()- Safe transient key generationhtax_debug_info()- Display debug information on archive pageshtax_get_group_type($taxonomy)- Get Term Groups group type suffix for taxonomyhtax_get_current_taxonomy()- Get current taxonomy from screen or request
Compatibility (includes/core/compatibility.php)
htax_check_compatibility()- Verify environment requirementshtax_display_compatibility_errors()- Show admin notices for issueshtax_check_file_integrity()- Verify all required files existhtax_display_file_integrity_errors()- Show missing file noticeshtax_can_load()- Master check before loading plugin
Activation (includes/core/activation.php)
htax_activate()- Main activation handlerhtax_activate_network()- Network-wide activationhtax_activate_single_site()- Single site activation; runs repair scans (htax_repair_visibility_cascade,htax_repair_orphaned_term_relationships,htax_repair_orphaned_sticky_meta), then callsHyperTaxonomy_Activator::schedule_index_rebuild()to schedule the full index rebuild after repairs complete, registers taxonomies, flushes rewrite rules, creates DB tableshtax_new_site_created()- New multisite site handlerhtax_deactivate()- Main deactivation handlerhtax_deactivate_network()- Network-wide deactivationhtax_deactivate_single_site()- Clears all transients and removes capabilities; data and tables are preservedhtax_add_security_headers()- Add security headers
Activator Class (includes/classes/class-activator.php)
HyperTaxonomy_Activator::activate()- Creates DB tables, adds indexes, sets version optionHyperTaxonomy_Activator::schedule_index_rebuild()- Scheduleshtax_rebuild_indexescron event 30 seconds in the future; idempotent (skips if already scheduled); called fromhtax_activate_single_site()after repair scans complete so the rebuild starts from clean data. The rebuild repopulates bothwp_htax_search_termsandwp_htax_post_text—wp_htax_post_textis truncated and rebuilt first so that strength scores inwp_htax_search_termsare derived from fresh post content;wp_htax_search_termsis cleared byclear_search_terms_for_rebuild(), which keeps external strength models’ scores for posts that will be reindexed (see Database Schema →model). All rebuild paths (rebuild_indexes(),rebuild_post_text_table(),ajax_bulk_rebuild()) select posts with the privateget_indexable_post_ids(), which readswp_postsdirectly: going throughWP_Querywould runpre_get_postshandlers, including HyperTaxonomy’s own hidden-term exclusion, which applies in WP Cron (no privileged user) and would otherwise drop posts with hidden terms from cron rebuilds.
Unregistered Taxonomy Handling
When a third-party plugin that registered a custom taxonomy is deactivated, WordPress fires no event that HyperTaxonomy can intercept. The taxonomy disappears silently on the next request. HyperTaxonomy handles this gracefully:
Feature suspension (immediate, zero data loss):
htax_is_taxonomy_enabled()returnsfalsefor any taxonomy that failstaxonomy_exists(), regardless of the stored setting. HyperTaxonomy features (archive expansion, search index writes, admin UI) are silently suspended.htax_get_enabled_taxonomies()filters out unregistered slugs before returning, so every caller automatically receives only currently-registered taxonomies. No individual call site needs ataxonomy_exists()guard.
Stored data (fully preserved):
wp_htax_term_relationshipsrows for the taxonomy’s terms are untouched.wp_htax_search_termsrows for the taxonomy’s terms persist (stale but harmless — archive expansion is gated onhtax_is_taxonomy_enabled()). They are cleaned up the next time any of those posts are reindexed for another reason, or on a manual index rebuild.- The
enabled_taxonomiesoption value remainstruefor the taxonomy. When the plugin is reactivated and the taxonomy re-registered,htax_is_taxonomy_enabled()immediately returnstrueand all features resume — no settings change required.
Cache handling:
- Most term-keyed transients (
htax_inc_,htax_cont_,htax_excl_,htax_groups_,htax_contrib_) are safe when their taxonomy is unregistered: they are only consumed when serving a term’s archive, which requires the taxonomy to be registered. Group member caches (htax_grp_{group_id}_{taxonomy}) are scoped by both group and taxonomy, so a disabled taxonomy’s entries are never consulted for active archives. Custom relationship type caches (htax_crel_{slug}_*andhtax_crel_rev_{slug}_*) are similarly safe: they are only consumed by callers that explicitly query by type slug, and those callers are always within active-taxonomy contexts. htax_get_redirect_target()validates the target’s taxonomy at resolution time. For an unregistered target taxonomy it returns false without overwriting the cache, so the redirect resumes automatically the next request after the taxonomy is re-registered. For an disabled in settings (registered but off in settings) target taxonomy it caches'none';on_taxonomy_enabled_state_changedclears all caches on re-enable so the redirect recovers then too.on_taxonomy_enabled_state_changed()callshtax_clear_all_caches()whenever any taxonomy transitions between enabled and disabled, giving a clean slate for the intentional-disable path.
Search index cleanup on settings save:
HyperTaxonomy_Search_Index_Manager::on_taxonomy_enabled_state_changed()hooksupdate_option_htax_settingsand compares old vs newenabled_taxonomies. Taxonomies that transition to disabled have theirwp_htax_search_termsrows deleted and all transient caches cleared. Taxonomies that transition to enabled have affected posts queued for reindex. Unregistered taxonomies cannot be toggled from the UI, so this path is not the primary cleanup mechanism for plugin deactivation — it covers intentional admin disables.HyperTaxonomy_Search_Index_Manager::on_relevance_weights_changed()also hooksupdate_option_htax_settings. For each enabled taxonomy it compares the effectivehierarchy,container, andredirectexplicit weights between old and new settings and, when any differ, queues every published post with terms in that taxonomy for reindex (those weights are multiplied into stored indirect-row strengths). Both handlers collect posts through the privateget_published_post_ids_in_taxonomy().
Resumption: No action required after the third-party plugin is reactivated. htax_is_taxonomy_enabled() and htax_get_enabled_taxonomies() will return the correct results on the first request after the taxonomy is registered again. Stale index rows are cleaned up incrementally; a manual rebuild via Settings → Search Index produces a fully clean state immediately.
Term Groups
Membership CRUD (includes/core/group-functions.php)
Membership CRUD + group taxonomy queries. Handles assigning terms to groups and querying the group taxonomy.
htax_get_term_groups()- Get groups assigned to a term as WP_Term[] ordered by name (cached ashtax_groups_{term_id}, eager reload)htax_get_term_group_ids( int $term_id ): int[]- Get IDs of groups a term belongs to; shares thehtax_groups_{term_id}transient cache withhtax_get_term_groups()htax_get_group_hierarchy()- Expand parent groups to include all descendants; uses_get_term_hierarchy('group')(WP object cache) for BFS — no extra DB query after first call per requesthtax_set_term_groups()- Replace all group assignments for a termhtax_set_group_term_count()- Recount a group’s direct members for a taxonomy and store it in_htax_count_{taxonomy}term meta; recounts on every call so bulk membership changes in one request stay accuratehtax_get_all_term_groups()- Get all Term Groups as group_id => WP_Term map
Groups-as-inclusion-sources (includes/core/term-groups.php)
Groups-as-inclusion-sources feature. Handles group-based inclusion and container relationships for term archives.
htax_get_terms_in_groups()- Get all terms from specified groups (with hierarchy expansion + per-group caching)htax_get_terms_from_included_groups()- Get WP_Term[] from a term’s included groupshtax_validate_and_filter_group_relationships()- Validate group relationships (hierarchy conflict detection)
Term Groups admin savers (includes/admin/term-edit.php)
htax_add_term_groups_field()- Term Groups assignment field (checklist). On the add form (priority 8) it renders above the collapsible toggle so it is always visible; on the edit form (priority 8) JS moves it before the native Description row. Not registered forgrouptaxonomy. Returns early whengroup_assignmentis disabled or no groups exist.htax_save_term_groups( int $term_id )- Save Term Groups assignments from the checklist field. Hooked onedited_term(priority 10) andcreate_term(priority 4), so it runs beforehtax_save_term_data()validates relationships. Admin-only (term-edit.php). Ignores requests withouthtax_groupsin$_POST.
Add/Edit form collapsibles (includes/admin/term-edit.php)
htax_add_form_collapsible_start()- Priority 9 on{tax}_add_form_fields. Outputs the “More options ▼” toggle button and opens the collapsible<div hidden>. JS moves the native Description<textarea>row into the collapsible as its first child. Not registered forgrouptaxonomy.htax_add_form_collapsible_end()- Priority 19 on{tax}_add_form_fields. Closes the collapsible and outer wrapper divs.htax_add_edit_form_collapsible_start()- Priority 9 on{tax}_edit_form_fields. Outputs the toggle as a<tr><th colspan="2">row; JS hides all<tr>siblings until the end-marker row and moves the native Description row into the collapsible zone.htax_add_edit_form_collapsible_end()- Priority 19 on{tax}_edit_form_fields. Outputs the empty end-marker<tr class="htax-edit-form-collapse-end">that bounds the collapsible range.
Attribution helpers (includes/admin/term-edit.php)
htax_build_container_group_attribution( array $group_ids, string $taxonomy ): array- Queries G→Tincludesrows wheresource_term_id IN ($group_ids)andsource_taxonomy='group'; returnsterm_id => [group_id, ...]map. Used for the includes-panel reverse path: terms that appear in this term’s archive because they are targeted by a group this term is a member of.htax_build_included_group_attribution( array $group_ids, string $taxonomy ): array- Mirror query: T→Gincludesrows wheretarget_term_id IN ($group_ids)andtarget_taxonomy='group'; returnsterm_id => [group_id, ...]map. Used for the containers-panel reverse path: terms whose archives include this term because they include a group this term belongs to.
Relationship Type Registry (includes/core/relationship-types.php)
Manages the in-memory registry of built-in and user-defined relationship types. Built-in types are registered on init at priority 0; user-defined types are loaded from wp_options on init at priority 5. All query sites use htax_relationship_type_sql_in() to expand a core type slug into an IN (...) SQL fragment that covers both the built-in slug and all user-defined slugs mapped to it.
Two new fields on each htax_custom_relationship_types entry (absent keys default to empty/false — no migration needed):
source_site(string) — URL of the peer site that originated this type via TermConnect sync. Empty = locally owned. Controls REST visibility: types withsource_siteset are excluded from/relationship-typesresponses (they are accessible at the originating site). Cleared byhtax_make_relationship_type_local().is_private(bool) — when true, excluded from both/relationship-typesand/term/{id}/relationshipsREST responses.
Registration API:
htax_register_relationship_type( string $slug, array $args ): void- Register a type (slug-keyed, invalidates slug cache; called directly or viahtax_relationship_typesaction)htax_get_registered_relationship_types( bool $skip_private = false ): array- Return full slug => args registry; when$skip_private = true, custom types withis_privateset are filtered out (built-in types are always included)htax_get_relationship_type( string $slug ): ?array- Return args for a single type, or nullhtax_get_relationship_type_slugs( string $core_type_slug ): string[]- Return all slugs whosecore_typematches (base types self-include; cached per core type)htax_relationship_type_sql_in( string $core_type_slug ): string- Return e.g."relationship_type IN ('includes','wrote')"— safe to interpolate into$wpdb->prepare()strings; slugs come from the registry and are passed throughesc_sql()
Persistence (user-defined types):
htax_set_custom_relationship_type( string $slug, array $args, bool $is_new = false ): true|WP_Error- Persist a new or updated user-defined type towp_optionsand sync the in-memory registry.$is_new = truerequires the slug to be unused (returnsWP_Error('slug_exists')if it already exists as a custom type);$is_new = falserequires the slug to already exist as a custom type (returnsWP_Error('not_found')otherwise). Additional validations: slug format; prevents overwriting core types; validatescore_typemappability; lockscore_typeon edit (passed value is ignored, existing value is preserved — no conversion between inclusion-based and custom after creation); forcore_type = 'includes', enforces thatsource_taxonomiesandtarget_taxonomiesare identical single-element arrays; strips'group'fromsource_taxonomies/target_taxonomies(groups are always implicitly allowed); enforces label and inverse label uniqueness — both are checked case-insensitively against every other registered type’s label and inverse label, and label must differ from inverse label; callshtax_delete_transients_by_prefix('htax_crel_{slug}_')andhtax_delete_transients_by_prefix('htax_crel_rev_{slug}_')after persistinghtax_make_relationship_type_local( string $slug ): bool- Clearsource_siteon a remote-delegated type, making it locally owned. Irreversible. Returns false for core types or unknown slugs.htax_remove_custom_relationship_type( string $slug ): bool- Remove a user-defined type fromwp_optionsand the in-memory registry; callshtax_delete_transients_by_prefix('htax_crel_{slug}_')andhtax_delete_transients_by_prefix('htax_crel_rev_{slug}_')to clear all cached relationship data for the deleted type; returns false for core typeshtax_remove_invalid_term_relationships( string $slug ): int- Delete rows that no longer satisfy the type’s constraints. Applies to both core and custom types. Conditions checked: group+group rows (always invalid);source_taxonomies/target_taxonomiesviolations (group side exempt); cross-taxonomy and self-relation rows forincludes/excludes;member_ofrows where source is a group or target is not a group;redirectself-redirect rows and chain rows (where the target is itself a redirect source — chain rows use a JOIN-based DELETE since MySQL forbids same-table subqueries in a plain DELETE). Pre-reads affected term IDs before deleting, then callshtax_delete_term_transients()for each. Returns total rows deleted. Called afterhtax_set_custom_relationship_type()and during activation for all four core types plus all custom types.
Query API:
htax_get_related_terms( int $term_id, string $type_slug, bool $include_redirect_data = false, bool $skip_if_private = true ): WP_Term[]- Get related WP_Term objects for a term filtered by exact slug (forward direction). Non-core custom types only — returns[]immediately for built-in slugs. Also returns[]when$skip_if_private = true(default) and the type hasis_privateset; passfalsefor local UI that legitimately needs private data. For non-core types, group targets are expanded to their member terms (viahtax_get_terms_in_groups()). Direct targets are batch-fetched per taxonomy. ReturnsWP_Term[]indexed by term_id. Cached ashtax_crel_{slug}_{term_id}(no TTL). Invalidated by:htax_add_term_relationship/htax_remove_term_relationship(source’s cache),htax_save_term_relationshipsedit form path (edited term’s own cache + all changed other-side terms),htax_delete_term_transients(term’s own cache),htax_invalidate_term_caches_on_delete(source terms whose cache pointed to the deleted term), type-prefix clear on type save/delete, andhtax_invalidate_group_relationship_cacheswhen group membership or group hierarchy changes.htax_get_reverse_related_terms( int $term_id, string $type_slug ): WP_Term[]- Get WP_Term objects that have a relationship of this type pointing to this term (reverse direction). Non-core custom types only — returns[]immediately for built-in slugs. For non-core types, group sources are expanded to their member terms. ReturnsWP_Term[]indexed by term_id. Cached ashtax_crel_rev_{slug}_{term_id}(no TTL). Invalidated by:htax_add_term_relationship/htax_remove_term_relationship(target’s cache),htax_save_term_relationshipsedit form path (edited term’s own cache + changed fwd targets’ rev caches),htax_delete_term_transients,htax_invalidate_term_caches_on_delete(target terms whose cache contained the deleted term), type-prefix clear on type save/delete, andhtax_invalidate_group_relationship_cacheswhen group membership or group hierarchy changes.htax_get_relationship_types_for_taxonomy( string $taxonomy ): array- Return slug-keyed user-defined types whereshow_in_uiis true andsource_taxonomiesis empty or contains the given taxonomy; used by term-edit UI and the save orchestrator to iterate visible types for a term
Term relationship write operations:
htax_add_term_relationship( int $source, int $target, string $type_slug ): true|WP_Error- INSERT IGNORE a relationship row; valid for'includes','excludes', and all non-core types;'redirect'and'member_of'trigger_doing_it_wrong(). Validates term existence; enforces group-aware taxonomy constraints (group+group rejected; group side is exempt from source/target taxonomy restrictions; includes-mapped types require both non-group sides to be in the same taxonomy; includes-mapped types reject self-inclusions). Invalidates per-term caches, fires type-specific change actions (htax_includes_changed/htax_containers_changedfor'includes',htax_excludes_changedfor'excludes'), and fireshtax_relationship_saved.htax_remove_term_relationship( int $source, int $target, string $type_slug ): bool- DELETE a relationship row; same type restrictions ashtax_add_term_relationship(); fires type-specific change actions andhtax_relationship_deletedon success.
Initialization:
htax_register_builtin_relationship_types(): void- Register built-in types in order:member_of → includes → excludes → redirect; fireshtax_relationship_typesaction for code-based registration. Registration order determines the display sequence in the Built-in Relationship Types table on the Term Relationship Types admin page; all runtime lookups are slug-keyed and unaffected by order.htax_load_custom_relationship_types(): void- Load stored user-defined types fromwp_optionsinto the registry (init priority 5)
Term Relationships (includes/core/term-relationships.php)
Low-level readers — direct DB / cache wrappers:
htax_get_direct_rel_term_ids( int $term_id, string $slug )- Get direct forward target IDs for a relationship type: raw T→U rows for the given slug, non-group targets only (cached ashtax_direct_rel_{slug}_{term_id}). Valid for'includes','excludes', and non-core types. Calling with'member_of'or'redirect'triggers_doing_it_wrong()and returns[]— usehtax_get_term_groups()orhtax_get_redirect()instead.htax_get_reverse_rel_term_ids( int $term_id, string $slug )- Get direct reverse source IDs for a relationship type: raw A→T rows for the given slug, non-group sources only (cached ashtax_reverse_rel_{slug}_{term_id}). Same slug restrictions ashtax_get_direct_rel_term_ids.htax_get_direct_rel_group_ids( int $term_id, string $slug )- Get direct forward group target IDs for a relationship type: raw T→G rows for the given slug, group targets only (cached ashtax_direct_rel_grp_{slug}_{term_id}). Same slug restrictions ashtax_get_direct_rel_term_ids.htax_get_reverse_rel_group_ids( int $term_id, string $slug )- Get direct reverse group source IDs for a relationship type: raw G→T rows for the given slug, group sources only (cached ashtax_reverse_rel_grp_{slug}_{term_id}). Same slug restrictions as above.htax_get_included_term_ids( int|WP_Term $term, bool $include_redirect_data = false )- Get IDs for the second OR clause ofhtax_build_expanded_tax_query: included terms plus all their descendants. Included terms’ children are pre-expanded here because that clause usesinclude_children => false. The primary term’s own children are not pre-expanded here — they are handled by the first OR clause (which usesinclude_children => is_taxonomy_hierarchical). Only called fromhtax_build_expanded_tax_query.
Resolved readers — multi-path, WP_Term objects:
htax_get_included_terms( int|WP_Term $term, bool $include_redirect_data = false, bool $include_disabled = false )- Returns empty whenterm_relationshipsis disabled for the taxonomy unless$include_disabled(search index callers only); the cached set is feature-independent. Get WP_Term[] of included terms indexed by term_id (cached ashtax_inc_{term_id}, eager reload). No TTL — persists until explicitly deleted byhtax_delete_term_transients().false= cold (never computed or evicted);[]= warm with no relationships. Empty result is stored as[]so that a warm cache with no inclusions never triggers a DB query.htax_get_container_terms( int|WP_Term $term, bool $include_disabled = false )- Sameterm_relationshipsgating and$include_disabledopt-out (search index callers). Get WP_Term[] of container terms indexed by term_id (cached ashtax_cont_{term_id}, lazy-load; 3 paths: direct, container groups, group inclusion). No TTL — persists until explicitly deleted byhtax_delete_term_transients(). Samefalsevs[]sentinel convention ashtax_inc_{term_id}.htax_get_synonym_terms( int|WP_Term $term )- Get WP_Term[] of terms with a bidirectionalincludesrelationship (A→B and B→A). Display utility — not required for archive expansion. Two-tier cache: static per-request array + persistent transienthtax_syno_{term_id}(TTL=0). Invalidated byhtax_delete_term_transients()orhtax_delete_term_transients_batch(); re-warmed asynchronously via thehtax_async_rebuild_term_cachescron event.htax_get_excluded_term_ids( int $term_id, bool $include_redirect_data = false ): int[]- Get excluded term IDs (cached ashtax_excl_{term_id}); returns[]for redirect terms unless$include_redirect_datais true
Redirect suppression pattern: All logical relationship getters suppress their results when the source term is a redirect and the taxonomy has the redirects feature enabled. Redirect terms preserve their stored rows in the DB and the cache, but callers see an empty result by default. Admin display passes $include_redirect_data = true to show the data grayed-out. The save handler (htax_save_term_relationships) skips processing for redirect terms to prevent the disabled form fields from wiping preserved data.
Functions that accept $include_redirect_data:
htax_get_included_terms( $term, bool $include_redirect_data = false, bool $include_disabled = false )htax_get_included_term_ids( $term, bool $include_redirect_data = false )htax_get_synonym_terms( $term, bool $include_redirect_data = false )— also filters redirect terms out of the result sethtax_get_excluded_term_ids( int $term_id, bool $include_redirect_data = false )htax_get_terms_from_included_groups( int $term_id, string $taxonomy, bool $include_redirect_data = false )htax_get_related_terms( int $term_id, string $type_slug, bool $include_redirect_data = false )— non-core custom types only; returns[]for built-in slugshtax_get_reverse_related_terms( int $term_id, string $type_slug )— non-core custom types only; returns[]for built-in slugshtax_get_sticky_posts_for_term( int $term_id, bool $include_redirect_data = false )
Redirects:
-
htax_get_redirect_target( $term_id )- Get redirect target as WP_Termfalse; applies two taxonomy guards: (1) if the target taxonomy is disabled in settings (registered but off in settings), caches 'none'— safe becauseon_taxonomy_enabled_state_changedcallshtax_clear_all_caches()on re-enable; (2) if the target taxonomy is unregistered, returns false without touching the cache, so the cached WP_Term (or the DB row) remains intact and the redirect resumes automatically when the plugin re-registers the taxonomy htax_is_redirect( $term_id )- Check if term is redirect (returns true regardless of feature flag — use in combination withhtax_taxonomy_has_feature($taxonomy, 'redirects')when suppression semantics are needed)htax_get_all_redirect_term_ids(): int[]- Bulk-fetch all source term IDs with a redirect row; cached per request (htax_request_cache( 'redirect_term_ids' )) and as thehtax_redirect_termstransient, both cleared byhtax_clear_redirect_term_ids_cache()onhtax_redirect_changedandhtax_redirect_removed(and byhtax_clear_relationship_caches()/htax_clear_all_caches())htax_clear_redirect_target_cache( int $term_id = 0 )- Delete a term’shtax_redir_transient and its per-request copy (0 = every term’s per-request copy);htax_set_redirect()/htax_remove_redirect()call it before firing their actions so index handlers see the new statehtax_clear_redirect_term_ids_cache()- Clear thehtax_redirect_termstransient and its per-request copyhtax_get_ids_of_redirects_to( $term_id, $taxonomies )- Get terms redirecting to target (cross-taxonomy)htax_prewarm_redirect_cache()- Bulk-fetch all redirect rows in one query, then batch-fetch target term objects by taxonomy, and populatehtax_redir_{id}transients. Call before bulk index loops to convert O(n) lazy queries into 1 + 1-per-taxonomy queries.htax_set_redirect( int|WP_Term $term, int|WP_Term $target )- Set redirect with circular detection and chain resolutionhtax_remove_redirect( $term_id )- Remove redirect from termhtax_save_redirect( $term_id, $tt_id )- Save redirect settings (called by orchestrator)
Write operations:
Save orchestration:
htax_save_term_data( $term_id, $tt_id )- Orchestrator that validates nonce once, checks permissions conditionally based on enabled features and form data, then calls all save functions (runs on bothedited_{$taxonomy}at priority 20 andcreate_{$taxonomy}at priority 5 for early AJAX compatibility)htax_save_term_relationships( $term_id, $tt_id )- Save include/container relationships (called by orchestrator, uses dual nonce verification)htax_save_group_relationships( $term_id, $taxonomy, $direction, $type_slug, $raw_value )- Save container or included group memberships for a term; called from withinhtax_save_term_relationships()for both directions and for each visible custom typehtax_save_archive_settings( $term_id )- Save archive display settings; skips when customize_archives disabled (called by orchestrator)HyperTaxonomy_Term_Icons::save_term_icon( $term_id )- Save term icon if enabled for taxonomy (called by orchestrator)
Cleanup:
htax_cleanup_deleted_term( $term_id, $tt_id )- Ondelete_term: removes all relationship rows where the term is source or target; clears_htax_auto_redirectmeta from any terms that were redirecting to this term; clears_htax_show_descriptionand_htax_is_hiddenmeta from the deleted term; deletes the_htax_sticky_{term_id}post meta entry and its transienthtax_cleanup_bulk_deleted_terms( $tt_id )- Clean up bulk deletions (auto-detects taxonomy from tt_id)htax_cleanup_hierarchy_conflicts( $term_id, $tt_id, $taxonomy )- Remove conflicts when hierarchy changeshtax_remove_duplicate_redirects(): int- Removes redirect rows where a source term has more than one target (schema allows it; write-time code prevents it, but direct DB manipulation could create this state); keeps the row with the lowesttarget_term_id(deterministic tiebreaker) and deletes the rest; idempotenthtax_cleanup_symmetric_relationship_duplicates(): int- For each symmetric custom relationship type, finds A→B + B→A duplicate row pairs and deletes the one wheresource_term_id > target_term_id, keeping a single canonical row per pair; called byhtax_repair_orphaned_term_relationships()and on activation; idempotenthtax_repair_orphaned_term_relationships(): int- Activation-time scan; (1) removes rows whose source term no longer exists, (2) removes rows whose target term no longer exists, (3) removes stale_htax_auto_redirectmeta from terms with no redirect row, (4) callshtax_remove_invalid_term_relationships()for all four core types and every user-defined type enforcing taxonomy constraints and self-relation rules, (5) callshtax_remove_duplicate_redirects()to enforce the one-target-per-source invariant, (6) callshtax_cleanup_symmetric_relationship_duplicates()for all symmetric custom types; returns total rows deleted; idempotent
Symmetric custom relationship types: User-defined types with symmetric: true have identical forward and inverse labels (e.g. “Related”). The term edit UI collapses both direction containers into one merged container: reverse pills (rows where another term points to this one) are shown with a dashed border and a ← arrow after the label; forward pills (rows created from this term’s edit page) are shown with a → arrow after the label. Adding via + always creates a forward row; removing either direction deletes the appropriate row. The save handler strips any forward-direction term IDs that already appear in the reverse direction, preventing A→B + B→A duplicates at write time. The autocomplete excludes all pills (both directions) so users cannot add a term already related in either direction.
Term Links Functions (includes/core/term-links.php)
Owned by TermConnect. See the TermConnect documentation.
Term Visibility (includes/core/term-visibility.php)
Hidden term logic: permission checks, write-time cascade, and frontend query filtering. Applies to all taxonomies including group when the visibility feature is enabled for it (Settings → Features → Term Groups → Enable Term Group visibility options).
Read / permission checks:
htax_is_term_hidden( $term_id )- Returns true if the term has_htax_is_hiddenmeta sethtax_can_see_hidden_terms( $taxonomy = '' )- Returns true if the current user holdsmanage_term_visibilitycapability or meets the site-wide role threshold (editors / all / none)htax_get_all_hidden_term_ids()- Returns all hidden term IDs from the DB; cached via persistent transient; invalidated on any visibility changehtax_get_visible_term_ids( $term_ids )- Filters an array of term IDs, removing hidden ones for unprivileged usershtax_get_visible_wp_descendants( int $term_id, string $taxonomy )- Returns WP descendants of a term excluding those marked hidden; relies on write-time cascade for completeness Cascade writes:htax_set_descendant_visibility( int $term_id, string $taxonomy, bool $hide )- Marks all WP descendants hidden or visible; fireshtax_term_visibility_changedper changed descendant Hook callbacks:htax_cascade_hidden_on_reparent( int $term_id, int $tt_id, string $taxonomy )-edited_termhandler (priority 21); cascades hidden to a term and its descendants when reparented under a hidden parenthtax_cascade_hidden_on_create( int $term_id, int $tt_id, string $taxonomy )-created_termhandler (priority 21); marks a newly created term hidden when its parent is hiddenhtax_repair_visibility_cascade(): int- Activation-time scan; re-cascades hidden status to any descendants of hidden terms that drifted out of sync while the plugin was inactive; returns count of corrected termshtax_hide_terms_in_tax_query( WP_Query $query )-parse_tax_queryhook (runs after WordPress converts every taxonomy query var —tag,cat,tag__in,taxonomy/term,tax_query, REST filters — into tax clauses); for users who cannot see hidden terms, removes hidden terms from every positive (IN/AND) clause viahtax_strip_hidden_terms_from_tax_clauses(), so hidden terms behave as if they did not exist: a clause left empty or anANDrequiring a hidden term matches nothing,include_childrenexpands to visible descendants only,NOT IN/EXISTSclauses are untouched. Posts carrying hidden terms are never excluded — they stay discoverable through their other terms and in unfiltered lists. The sticky fetch (post__in, no tax clauses) is unaffected, so thehtax_sticky_*cache is viewer-independent Admin save:htax_save_hidden_term( $term_id )- Save term visibility setting (called by orchestrator)
Archive Expansion
Term archives (includes/core/term-archives.php)
htax_get_enabled_archive_taxonomy()- Returns the current archive’s taxonomy slug if HyperTaxonomy is enabled for it; null otherwise. Used as a guard by the sort-order block and archive query handlers.htax_build_expanded_tax_query( int $term_id, string $taxonomy, ?WP_Term $term_obj = null )- Build OR tax_query for a term’s archive expansion; returns null if no HyperTaxonomy contributions or ifterm_relationshipsfeature is not enabled for the taxonomy. Called by bothhtax_modify_archive_queryandhtax_expand_secondary_query.htax_modify_archive_query()- Modify main archive query for inclusions/redirects (delegates expansion tohtax_build_expanded_tax_query). When an expandedtax_queryis applied, WordPress’s built-in per-taxonomy query vars (tag_id,cat,category_name,tag,term) are cleared to prevent WP from constructing a redundant native clause. Thetaxonomyquery var is intentionally preserved so thatis_tax(), breadcrumb plugins, and SEO plugins continue to identify the archive context correctly.htax_handle_redirects()- Handle 301 redirects for termshtax_get_contributing_terms_for_current_page()- Get terms contributing to archivehtax_get_sticky_term_from_tax_query( WP_Query $query )- Extract a single sticky-eligible term from a RESTtax_query; returns['term_id', 'taxonomy']when the query has exactly oneIN/ANDinclusion entry with one term ID in acustomize_archives-enabled taxonomy, null otherwisehtax_modify_archive_query_for_stickies()- Add sticky posts to archive query; handles both regular term archives (main query, viaget_queried_object()) and REST API requests (viahtax_get_sticky_term_from_tax_query()); stores state on the query object itself via$query->set()so it works for non-global REST querieshtax_get_sticky_posts_for_term()- Get sticky post objects for a term (taxonomy derived from the term). Thehtax_sticky_{term_id}cache holds only the validated post IDs — an empty list is cached too — and posts are loaded fresh on each call (all indexed post types)htax_invalidate_sticky_cache_on_meta( $meta_id, $object_id, $meta_key )-added_post_meta/updated_post_meta/deleted_post_meta: clearshtax_sticky_{term_id}when any_htax_sticky_{term_id}key is written, by any writerhtax_invalidate_sticky_cache_for_post( int $post_id )- clears the sticky cache of every term the post is pinned to; hooked totransition_post_status(status actually changed),set_object_terms, anddeleted_term_relationships; filters bypost_status = 'publish'htax_prepend_sticky_posts()- Prepend sticky posts to query results; handles both main query archives and REST API requests; also recomputesmax_num_pagesfrom the full page size (WP derives it from the reduced page-1posts_per_page)htax_sticky_found_posts( $found_posts, $query )-found_postsfilter: adds the pinned count (htax_sticky_countquery var) back, since pinned posts are excluded from the regular query on every page. With pinning, page 1 queriesposts_per_page − Nregular posts and pages 2+ useoffset = (paged − 1) × posts_per_page − N, so no regular post is skipped; stickies beyond the max-sticky cap are not pinned and stay in their natural positionhtax_handle_group_archives()-pre_get_postshandler for group taxonomy archives; builds an OR tax_query viahtax_build_group_tax_query()and applies it to the main queryhtax_cleanup_sticky_on_delete( $post_id )- Ondelete_post: removes all_htax_sticky_{term_id}post meta entries for the deleted posthtax_repair_orphaned_sticky_meta(): int- Activation-time scan; finds all_htax_sticky_*post meta keys and deletes any whose term no longer exists; idempotent
Integration API (includes/core/integration-api.php)
Public integration API for third-party plugins.
htax_get_expanded_term_ids( int $term_id )- Returns the complete flat list of term IDs whose posts appear in a term’s archive: self + WP descendants (viahtax_get_visible_wp_descendants) + all HyperTaxonomy-contributed terms fromhtax_build_expanded_tax_query(inclusions pre-expanded to include their own WP descendants). WP descendants of the primary term are always added explicitly regardless of whether HyperTaxonomy contributions are present. Taxonomy is looked up automatically. Applieshtax_expanded_term_idsfilter.htax_expand_secondary_query( WP_Query $query )-pre_get_postshandler (priority 10) that expands single-term tax_query clauses for enabled taxonomies in secondary/third-party queries. Controlled byhtax_expand_termsquery var orexpand_secondary_queriesglobal setting. Skips the main archive query.htax_adjust_term_counts( array $terms, ... )-get_termsfilter. Replaces native WP counts with expansion-aware counts. Frontend only, active only whenexpand_secondary_queriesis on.
Frontend Display (includes/core/frontend.php)
Hidden term filters:
htax_filter_term_links()- Remove hidden terms from WordPress’s nativeterm_links-{taxonomy}HTML link listshtax_filter_get_the_terms()- Mark hidden terms in arrayshtax_filter_tag_cloud()- Remove hidden terms from tag cloud
Archive description:
htax_filter_title()- Add contributing terms to archive titlehtax_display_archive_description()- Display term descriptionhtax_get_allowed_description_html()- Define allowed HTML tagshtax_integrate_description_display()- Set up description hookshtax_maybe_display_archive_description()- Conditional displayhtax_filter_query_title_block()- Filter FSE Query Title block
Styles:
htax_frontend_styles()- Output frontend CSShtax_icon_border_radius_styles()- Dynamic icon border radius CSShtax_filter_post_class()- Add sticky post CSS classes
Icon / pill rendering:
htax_get_the_archive_icon( string $size, array $attr )- Get current term archive icon as responsive<img>with srcset; empty string on non-term archives or missing iconhtax_the_archive_icon( string $size, array $attr )- Echo wrapper forhtax_get_the_archive_icon()htax_get_term_icon_url( WP_Term|int $term, string $size )- Get term icon URL (accepts WP_Term object or term ID)htax_get_term_icon_html( WP_Term|int $term, string $size, array $attr )- Get term icon HTML (accepts WP_Term object or term ID)htax_get_icon_display_for_taxonomy( string $taxonomy )- Get icon display setting for taxonomyhtax_get_term_pill( WP_Term $term, array $args )- Get term as a styled pill with optional iconhtax_the_term_pills( int $post_id, string $taxonomy, array $args )- Display all post terms as styled pillshtax_add_icon_to_term_link( string $term_link, WP_Term $term, string $show_icons, bool $is_hidden )- Transform existing term link into pill
Text utilities:
htax_split_description()- Split description into fragments-
htax_truncate_text()- Truncate text with ellipsis htax_suppress_native_archive_description( string $description ): string-get_the_archive_descriptionfilter at priority 1. Returns empty string when_htax_show_description = '1', preventing a theme’sthe_archive_description()call from duplicating the description thathtax_display_archive_description()already outputs. WordPress skips the wrapper div entirely on empty return.
Search
Query Intercept (includes/core/search.php)
htax_handle_widget_post_search( WP_Query $query )-pre_get_postscallback; runs a search widget POST-mode search (detected via$_POST['htax_post_search'], normally restored from a results-page token — see Search State below); verifies the nonce unless the state came from a token, forcesis_search, runsget_posts_by_custom_sql()for the requested page with the form’shtax_posts_per_page/htax_max_results, and applies the page withhtax_apply_search_page_to_query()htax_handle_power_post_search( WP_Query $query )-pre_get_postscallback; runs a PowerSearch POST-mode search (detected via$_POST['htax_power_post_search']); verifies the nonce unless the state came from a token, validates minimum query length, builds config from the form’s hidden inputs (posts_per_page,max_results,match,near_country,search_in,orderby), runsperform_indexed_text_search()for the requested page and applies it withhtax_apply_search_page_to_query()
Search State — POST/Redirect/GET (includes/core/search-state.php)
POST-mode forms submit once; the results pages are GET requests carrying ?htax_q=<token>, so the theme’s /page/N/ links (which keep the query string), back/refresh, bookmarks and page caches work.
htax_redirect_search_submission()-initpriority 1; for a POST withhtax_post_search/htax_power_post_searchand a valid nonce, encodes every submitted field except nonces,_wp_http_refererand*_termdata(labels are rebuilt from term IDs) and redirects (303) to the posted URL minus/page/N/plushtax_q. An invalid nonce is left for the handlers to reject.htax_restore_search_state()-initpriority 0; on a GET withhtax_q, decodes the state and writes it (slashed, as WordPress slashes request data) into$_POST/$_REQUEST, so the handlers and the form-restoration code run unchanged; marks the request withhtax_is_search_state_request().htax_search_state_encode( array $state ): string/htax_search_state_decode( string $token ): ?array-z+ base64url(deflate(JSON)) when ≤HTAX_SEARCH_STATE_MAX_INLINE(1500) characters (durable, no storage); otherwisek+ 16-hex content hash of a transienthtax_ss_{hash}keptHTAX_SEARCH_STATE_TTL(30 days). Decoding caps the inflated size at 64 KB.htax_is_search_state_request( ?bool $set = null ): bool- whether this request’s state came from a token (verified at submission, so the handlers skip their nonce check — the nonce would have expired or belong to another visitor).htax_apply_search_page_to_query( WP_Query $query, array $post_ids, int $total, int $posts_per_page, int $paged )- sets the main query to exactly one engine page:post__inin order, indexed post types, no stickies, and query varhtax_search_total. With it,htax_search_page_post_limits()(post_limits) starts the LIMIT at row 0 — the LIMIT is kept because WordPress computesmax_num_pagesonly for limited queries — andhtax_search_page_found_posts()(found_posts) reports the true total.htax_get_search_page_number( WP_Query $query ): int- current page frompaged, orpageon singular page URLs.htax_filter_search_query_text()(get_search_query) - returns thehtax_search_textquery var whenswas cleared by an HT search, so theme titles show the query. While a search form renders (htax_track_search_form_rendering()onpre_get_search_form/get_search_form/pre_render_block/render_block_core/search; state inhtax_is_rendering_search_form()), it is applied on native?s=searches, and on HT results pages only when Site Search Integration (always_enhance_search) is on — otherwise that box would run WordPress’s own search, so it stays empty.
Search notices (includes/core/search.php)
htax_add_search_notice( string $code, string $message )/htax_get_search_notices(): array- per-request visitor messages ($GLOBALS['htax_search_notices'], keyed by code). Included asnoticesin widget and PowerSearch AJAX responses;htax_print_search_notices()(loop_start/loop_no_results, main search query, once) prints them on results pages.
Post Text Sync (includes/core/search.php):
htax_sync_post_text_row( int $post_id, WP_Post $post )-save_posthandler; upserts a row intowp_htax_post_textfor published, draft, pending, future, and private posts usinghtax_extract_post_body()onpost_content; deletes the row for trash and auto-draft. Skips revisions and autosaves. Draft rows allow the post editor to perform content-based tag suggestions against the same table public search queries use.htax_extract_post_body( string $html ): string- Converts rawpost_contentHTML to plain text for FULLTEXT indexing. Preserves semantically useful attribute values before stripping tags:<a href>URLs appended as[URL]after anchor text,<img alt>text, and<abbr title>/<acronym title>expanded forms appended after the abbreviated text. Then strips all remaining HTML withwp_strip_all_tags(). Struck-through content (<del>,<s>) is kept — authors use strikethrough as editorial rephrase and the wording remains meaningful.htax_sync_post_text_comment( int $post_id, int $new_count )-wp_update_comment_counthandler; updatescomment_countandlast_comment_gmtfor rows that already exist inwp_htax_post_text. No-ops if the post is not in the table (trashed, auto-draft, or not an indexed post type).
Search Enhancement:
htax_parse_orderby()- Parse orderby parameter string into arrayhtax_apply_orderby_to_wp_query()- Apply parsed orderby to WP_Query argumentshtax_enhance_search_query()- Native?s=search through the PowerSearch engine (always, or withhtax_search): leaves an emptysto WordPress; runs the requested page with the query’sposts_per_page(posts_per_rssfor search feeds), clearss, stores the text inhtax_search_text, and applies the page withhtax_apply_search_page_to_query()— the engine sorts by the full orderby, so the page keeps its orderhtax_get_search_results_per_page()- Default posts-per-page for all HT search forms; returnsget_option('posts_per_page', 20)so that the WordPress Reading setting is the single site-wide control. Shortcode-levelposts_per_pageattributes always override this.htax_get_default_match_mode()- Returns'all'or'any'fromhtax_settings[default_match_mode]; defaults to'any'.
URL Parameters:
htax_search=1- Trigger enhanced searchhtax_search_in- Comma-separated list: title, content, excerpt, tags, categorieshtax_search_mode- Search mode: phrase_first (default), phrase_only, words_any, words_allhtax_orderby- Sort order format: “field:order,field:order” (e.g., “relevance:desc,date:desc”)- Valid fields: relevance, date, modified, title, author, comment_count, last_comment, last_activity, ID
- Valid orders: asc, desc (defaults to desc if omitted)
- Relevance works with text searches only in enhanced search
last_commentsorts bylast_comment_gmt;last_activitysorts byGREATEST(post_modified_gmt, last_comment_gmt)— both columns are always present because all queries usewp_htax_post_textas the primary post source- Also accepted on term archive URLs to override per-term sort order (archive-valid fields only: date, modified, title, author, comment_count); emitted by
[ht_sort_order_options action="navigate"] - Example:
?s=test&htax_search=1&htax_orderby=date:desc,title:asc
Sort Order Picker (includes/shortcodes/class-sort-order.php)
Shared renderer and shortcode handler for the pill-based sort order picker component. Loaded before class-post-list.php since post list and search result shortcodes call htax_render_sort_order_picker() directly.
htax_sort_field_labels()- Returns the canonicalfield_slug => translated labelmap for all supported sort fields (date,modified,title,author,comment_count,last_comment,last_activity,relevance,ID)htax_enqueue_sort_order_assets()- Enqueueshtax-sort-orderCSS and JS; safe to call multiple times (checkswp_style_is/wp_script_isbefore enqueuing)-
htax_render_sort_order_picker( array $args )- Renders the complete[data-htax-sort-root]HTML block: toggle button, active-pill container with add button, picker popover, hidden value input. Arguments:fields(array or comma-string),state(expandedcollapsed), action(navigatesubmit ’’), default_value,input_name,text_collapsed,text_expanded,class htax_sort_order_options_shortcode( $atts )- Handler for[ht_sort_order_options]; defaults toaction="navigate"and auto-populatesdefault_valuefrom$_GET['htax_orderby']when action is navigate
JavaScript component (assets/js/htax-sort-order.js):
window.htaxSortOrder.init(root, opts)- Initializes a picker within a root element; returns{ getValue, setValue, destroy }- Auto-initializes all
[data-htax-sort-root]elements on DOMReady - Fires jQuery event
htax:sort-changeon the root element (bubbles) with the new serialized value as second argument data-htax-sort-action="navigate|submit"controls what happens on change; omit for event-only (admin/AJAX contexts)syncAddBtn()— hides the+button when all available fields are already active as pills
sort_order_options attribute on result shortcodes:
[ht_post_list sort_order_options="..."]— renders picker as sibling of.htax-post-list-wrapperinside.htax-post-list-block;htax-post-list.jshandleshtax:sort-changeby updatingdata-orderbyand callingloadPage($wrapper, 1)[ht_search_results sort_order_options="..."]— renders picker inside.htax-search-resultsbefore the results container. Both search widget and PowerSearch store their JS instance on the container via$results.data('htax-form-instance', this); the single delegatedhtax:sort-changehandler inhtax-search-widget.jsreads this reference and callsinstance.performSearch(1)regardless of which form type is in use. Pagination prev/next buttons use the same stored reference.
Filter Options Panel (includes/shortcodes/class-filter-options.php)
Collapsible filter panel supporting term, date range, and author filters. Works in three action modes: navigate (URL params on archives), submit (form submission), and event-only (AJAX result shortcodes). Loaded before class-post-list.php since post list, search widget, and PowerSearch shortcodes call its render and SQL-builder functions directly.
Key global functions:
htax_build_filter_sql( array $filter_state, array $filter_configs = [] ): string— Builds SQL WHERE clauses from a parsed filter state array. Usesptas thewp_htax_post_textalias (consistent across all three query contexts). Returns empty string when state is empty. Term filters usept.post_id IN (SELECT post_id …)subqueries againstwp_htax_search_terms; date filters usept.post_date_gmt/pt.post_modified_gmt; author filter usespt.post_author IN (…).htax_parse_filter_state_navigate( array $raw ): array— Sanitizes a filter state from$_GET['htax_filter']URL params. Term IDs areintval-cast; dates are validated withpreg_match; author IDs areintval-cast. Match type is always'any'in navigate mode.htax_parse_filter_state_ajax( string $json ): array— Parses filter state from a JSON string POSTed ashtax_filterin AJAX requests. Same sanitization as navigate variant; preservesterm_matchkeys from the client state.htax_resolve_filter_configs( array $raw_configs, array $filter_state, $archive_exclude_term, array $primary_seed_ids = [] ): array— Converts raw shortcode attribute arrays into fully resolved config arrays. Fetches term options from the DB, applies pool include/exclude rules, merges initial selection from filter state and seed IDs, and forces seeded terms into the pool. Returns one config array per filter group ready for rendering.htax_render_filter_options( array $configs, array $args, array $filter_state ): string— Renders the complete[data-htax-filter-root]HTML block: toggle/clear header, body with one group per config, hidden value inputs, anddata-htax-filter-state/data-htax-filter-initialJSON attributes.htax_parse_inline_filters( string $filters_string, string $filter_options_string = '' ): array— Converts thefilters=/filter_options=inline shortcode attributes into raw config arrays compatible withhtax_resolve_filter_configs(). Keywords:date_published,date_modified,author, or any taxonomy slug. First taxonomy slug is markedprimary=true. Position-alignedfilter_optionsvalues:singlefor date type,group:Nfor term pool.htax_build_archive_filter_mods( array $filter_state ): array— Converts filter state intoWP_Querymodification arrays:tax_query_additional(merged into existingtax_querywithrelation=AND),date_query,author__in.htax_apply_archive_filter( WP_Query $query )—pre_get_postscallback at priority 11. Reads$_GET['htax_filter'], parses viahtax_parse_filter_state_navigate(), builds mods viahtax_build_archive_filter_mods(), and merges them into the main query. Only runs on the main front-end query with aqueried_object(i.e. actual archive pages).htax_enqueue_filter_options_assets()— Enqueueshtax-filter-optionsCSS and JS; safe to call multiple times.
Shortcode handlers (registered on init):
htax_filter_options_shortcode( $atts, $content )—[ht_filter_options]parent; collects child configs captured duringdo_shortcode($content), resolves them, and renders the panel. In archive context, seeds the primary filter’s initial selection from the current term’s child/parent terms.htax_filter_terms_shortcode( $atts )—[ht_filter_terms]; pushes a raw config onto the active capture stack.htax_filter_date_published_shortcode( $atts )—[ht_filter_date_published]; pushes a date config.htax_filter_date_modified_shortcode( $atts )—[ht_filter_date_modified]; pushes a date config.htax_filter_author_shortcode( $atts )—[ht_filter_author]; pushes an author config.
JavaScript component (assets/js/htax-filter-options.js):
- Each
[data-htax-filter-root]element gets aFilterPanelinstance on DOMReady. - State is stored in
this.state(current) andthis.initial(initial/seed);isDirty()compares them to control the “Clear filters” button visibility. commit( newState )— updates state and fireshtax:filter-change(event mode), navigates (navigate mode), or submits the ancestor form (submit mode).resetToInitial()— restores all UI controls to initial values and callscommit(initial).- Three delegated
htax:filter-changehandlers wire up to result shortcodes:- Post list: sets
$wrapper.data('htax-filter', newState)then triggershtax:load-pageon.htax-post-list-wrapper - Search widget: sets
$results.data('htax-filter', newState)then callsHTAXSearchWidget.forms[formId].performSearch(1) - PowerSearch: sets
$results.data('htax-filter', newState)then calls theFulltextSearchForminstance’sperformSearch(1)
- Post list: sets
Filter state integration in AJAX handlers:
- All three AJAX search handlers (
ajax_load_pagein post list,ajax_search_postsin search widget and PowerSearch) read$_POST['htax_filter'], parse viahtax_parse_filter_state_ajax(), build SQL viahtax_build_filter_sql(), and pass the result as$extra_where/user_filter_sqlto the query layer. - SQL alias
pt(forwp_htax_post_text) is consistent across all three query contexts, so filter SQL is interchangeable.
filters/filter_options inline attributes on result shortcodes:
- Available on
[ht_post_list]and[ht_search_results](the unified results container used by both search widget and PowerSearch). - When set,
htax_parse_inline_filters()→htax_resolve_filter_configs()→htax_render_filter_options()is called at render time (alwaysaction=""). - The filter panel HTML is prepended as a sibling of the results wrapper inside a block div (
.htax-post-list-block,.htax-search-results-block). The JS filter-change handlers rely on this sibling relationship. [ht_post_list]also passes resolvedfilter_termsIDs as$primary_seed_idsso the filter panel UI matches the initial page-load query result.
ATCF Integration (includes/integrations/class-atcf-integration.php)
Conditionally loaded when function_exists( 'atcf_get_term_ids_within_radius' ). Hooks:
htax_pre_search_fields— receives the full search query spec{relation, fields, groups, text_search}and resolves anydistance-type fields in$query['fields']totype:"hidden"resolved fields containing matching term IDs before the SQL query runs. Prefers JS-provided coordinates (_atcf_lat/_atcf_lngrequest params) over postal-code centroid lookup; falls back toatcf_get_ip_location(). Anyexpand_typesvalue from the distance field is passed through unchanged to the resolved field;normalize_expand_types()in the search widget class handles final normalization at query time.htax_allowed_field_types— adds'distance'to the accepted field types so the AJAX handler does not reject distance fieldshtax_search_proximity_where—build_proximity_where()turnsnear:/-near:tokens into a WHERE condition for PowerSearch and the search widget (see below)htax_search_proximity_relevance—build_proximity_relevance()scores posts by distance fornear:-only searches (PowerSearch$near_onlypath:proximity_scoreselect column, HAVING1 = 1; widget text-only branch). Both builders shareresolve_proximity(), memoized per request (proximity tokens + hidden-term visibility), which resolves references once, records unresolved-clause notices once, and returns in-range term distances (get_in_range_term_distances(), from ATCF’sdistance_km)htax_term_locations— provides ATCF location rows to HT’s filter; consumed byhtax_get_term_locations()for REST responseshtax_term_locations—htax_filter_rest_prepare_termreads location data from this filter directly (rather than fromrest_prepare_{$taxonomy})
The integration touches no ATCF database tables directly — all location data is retrieved via ATCF’s public API (atcf_get_locations_within_radius(), atcf_get_term_ids_with_locations(), atcf_get_postal_centroid(), ATCF_Locations::get_aggregated()). The only direct DB queries HT performs are against WP’s own term_relationships and term_taxonomy tables to map term IDs to post IDs.
The [htax_distance_field] shortcode is registered unconditionally (so the tag is always recognized in content). When ATCF is inactive at render time, the shortcode outputs a notice for admins and nothing for other users.
JavaScript event fired by [htax_distance_field]:
atcf:termFilterInserted— dispatched ondocumentafter the distance filter element is rendered into the form.e.detail.fieldNameis the field’snameattribute;e.detail.formIdis the enclosing[ht_search_form]id.
htax_search_proximity_where filter (PowerSearch + search widget):
Receives (string $where, array $proximity, string $post_column) and returns an SQL condition (no leading AND; '' = none). Both perform_indexed_text_search() (PowerSearch) and get_posts_by_custom_sql() (search widget) fire it when proximity tokens are present and append the result to the WHERE clause — before ORDER BY / LIMIT, so pages and totals are computed on the filtered set and the relative order of the remaining posts is unchanged. PowerSearch resolves the condition before its result-cache lookup and includes it in the cache key, since near:me depends on the viewer’s location.
The ATCF integration’s build_proximity_where() keeps a post when at least one of its terms in an eligible taxonomy (public taxonomies enabled in ATCF) has a location in range of some positive clause. Posts with no eligible terms, or whose eligible terms have no locations, are dropped — their location is unknown. Positive clauses are unioned (in-range term IDs from every resolvable clause); unresolvable clauses are ignored, and when none resolves there is no positive condition. Negated clauses (-near:) drop posts in range of any of them. Hidden terms are left out for viewers who can’t see them.
In-range term IDs come from ATCF’s API (atcf_get_locations_within_radius()). posts_with_terms_sql() resolves them to term_taxonomy_ids and emits a correlated EXISTS (SELECT 1 FROM term_relationships WHERE object_id = <post> AND term_taxonomy_id IN (…)), which uses the table’s (object_id, term_taxonomy_id) primary key per candidate post and lets an ORDER BY … LIMIT query stop once its page is full. On a 300k-post / 2.7M-relationship test set this measured ~70 ms for page 1 and ~120 ms at offset 5000 (vs ~0.5 s for an IN (SELECT …) semi-join and ~0.7 s / 90 MB for fetching the full ordered result and filtering it in PHP), with identical rows.
Proximity syntax parsed by htax_parse_search_query() (ATCF-gated):
near:taxonomy:slug[/radius] — reference: term's ATCF locations (+ WP descendants + HT inclusions)
near:X[/radius] — X checked against country's postal format (uppercase):
match → postal centroid; no match → slug lookup in all eligible taxonomies
near:me[/radius] — reference: user location (_atcf_lat/_atcf_lng or IP geo fallback)
-near:... — negation; excludes class D posts
Default radius: 10, in the ATCF distance unit (atcf_distance_units). Country context (for postal format check and centroid lookup) is set by the near_country shortcode/block attribute (default 'US'). When ATCF is inactive, near: text is not extracted and falls through to FULLTEXT search unchanged.
Reference point aggregation for near:taxonomy:term:
Reference locations are resolved via ATCF_Locations::get_aggregated(), which expands the scope to include WP taxonomy descendants (get_term_children()) and HT inclusion terms (htax_get_included_term_ids()), and excludes HT hidden terms (htax_get_all_hidden_term_ids()). This means near:geo:europe finds locations across all European child terms, not just direct ATCF locations on the “europe” term itself.
REST API (includes/core/rest-api.php)
Field builders:
htax_rest_term_stub( WP_Term $term )- Build minimal{ id, name, slug, taxonomy, link }object for REST field valueshtax_rest_build_icon_data( WP_Term $term )- Buildhtax_iconREST field value (null when no icon set)htax_rest_build_redirect_data( WP_Term $term )- Buildhtax_redirectREST field value (null when not a redirect)htax_rest_build_includes_data( WP_Term $term )- Buildhtax_includesREST field value (WP descendants + direct HyperTaxonomy inclusions, including inclusion-based custom types)htax_rest_build_synonyms_data( WP_Term $term )- Buildhtax_synonymsREST field value (null when no synonyms)ATCF_REST_API::format_term_locations( int $term_id )- Owned by ATCF. Returnshtax_locationsREST field value for a term: Schema.org Place array, or null when no locations. Caches per term_id for request lifetime. HT calls this with amethod_existsguard; it usesATCF_Locations::get_by_term_id()internally so no taxonomy parameter is needed.htax_rest_build_relationships_data( WP_Term $term )- Buildhtax_relationshipsREST field value; storage-only custom types only (core_type = null, non-private); inclusion-based types excluded (they travel viahtax_includes); returns[{relationship_type, target_slug, target_taxonomy}]htax_rest_build_term_entry( WP_Term $term )- Build a minimal term stub{id, slug, name, taxonomy, link}used insidehtax_includesstubs andhtax_synonymsentries
Term endpoint filters & registration:
htax_filter_rest_terms_query()- Exclude hidden terms from REST collection responses (always enforced)htax_filter_rest_prepare_term()- Block hidden terms with 404 (always enforced); inject sparsehtax_*fields when data exists. Enrichment fields are also skipped when_locale=useris present in the request —wp.apiFetchalways adds this param so the block editor never receives the enrichment fields (it reads HyperTaxonomy display data from PHP-localizedhtaxTermMetainstead); external REST clients, which do not send_locale=user, receive full enrichment. Hidden-term 404 enforcement fires unconditionally regardless of_locale=user.htax_register_rest_term_filters()- Hook the above two functions onto every publicshow_in_resttaxonomyhtax_filter_sitemap_terms_query()- Exclude hidden term archives from XML sitemaps
Posts endpoint extensions:
htax_rest_add_version_header( WP_REST_Response $result )- AddsX-HyperTaxonomy: {version}header to every REST response; used by peer sites for HyperTaxonomy detection. Conditionally registered inhooks/filters.phpbased on the HyperTaxonomy Presence Header Advanced setting (default on)htax_rest_register_post_extensions()- Registershtax_*collection params, the scoring query filter, and thehtax_score/htax_matched_term_slugsREST fields on theposttype; hooked onrest_api_inithtax_register_tag_suggestions_rest_route()- RegistersGET /htax/v1/suggested-tags; requiresedit_posts; hooked onrest_api_inithtax_rest_suggested_tags( WP_REST_Request $request )- Callback for/htax/v1/suggested-tags. Acceptspost_id(required),taxonomy(defaultpost_tag, any flat taxonomy supported), andper_page. Fetches the post’s text fromwp_htax_post_text(falls back to live post), applies ignore words per-field (word-boundary regex from Settings → Advanced), then counts occurrences of each unassigned term’s name in title, excerpt, and body independently. Returns terms in three groups — title matches first, then excerpt-only, then body-only — each group sorted by count descending, sliced toper_page. Response mirrors WP REST term format so Gutenberg’s tag selector can consume it directly. The Gutenberg panel registers onePluginDocumentSettingPanelper enabled flat taxonomy (including custom ones) and positions each panel immediately after its taxonomy section in the sidebar viadomReadyDOM manipulation.htax_register_term_meta_route()- RegistersGET /htax/v1/term-meta; requiresedit_posts; hooked onrest_api_init. Used by the JS autocomplete subscriber to fetch badge metadata for terms added mid-session.htax_rest_term_meta( WP_REST_Request $request )- Callback forGET /htax/v1/term-meta. Acceptstaxonomy(required, must exist) andids(required, comma-separated integers, capped at 50). Returns{ terms: [{id, name, slug, is_redirect, is_hidden, inc_count, cont_count, included_names, container_names, redirect_target_name}] }— only terms with at least one HT flag are included; terms with nothing to show are omitted.htax_rest_register_post_htax_params( array $params, string $post_type )-rest_post_collection_paramsfilter: registershtax_includes,htax_match,htax_weightsas accepted params on/wp-json/wp/v2/postshtax_rest_apply_htax_post_query( array $args, WP_REST_Request $request )-rest_wp_post_queryfilter: whenhtax_includesis present, reads primary and included term IDs, queries the search index (relationship_type='direct'only), scores posts, restricts the WP_Query viapost__in, and stores normalized scores in$_htax_rest_post_scoresfor the field callbacks. The standard?tags=/?categories=params remain in effect and AND withpost__in.htax_rest_get_post_score( array $post )- REST field callback forhtax_score; returns 0–1 float or nullhtax_rest_get_post_matched_slugs( array $post )- REST field callback forhtax_matched_term_slugs; returns string[] (remote term slugs derived from matched term IDs) or nullhtax_term_peersfield — registered by TermConnect (when active) on post REST responses. Returns peer connection data for all terms on returned posts. See TermConnect documentation.
_fields awareness. All HT-added fields are gated by htax_rest_field_requested( ?WP_REST_Request $request, string $field ): bool. Returns false if: (a) the field is globally disabled by the Relationship Data in REST Responses setting (htax_includes, htax_synonyms), OR (b) a _fields parameter was supplied and does not include this field. Returns true when _fields is absent (backward compatible). Callers should specify _fields to avoid unused computation; TermConnect does this in all outbound requests.
All term fields now available on peer collection responses. Every field listed in the build-helpers above is available on both individual and peer collection responses when requested in _fields. Relationship-specific fields (htax_includes, htax_relationships) are additionally gated by the Peer Relationship Delegation API Advanced setting. htax_synonyms and htax_includes are gated by the Relationship Data in REST Responses setting. htax_locations is gated by the share_locations feature flag.
htax_relationships is also available on individual term responses (not just peer collection), gated by _fields.
htax_relationship_types_admin_footer action. Fires at the bottom of the relationship types admin page. TC hooks here to inject the “Discover from peer site” UI.
Scoring: Score = sum(weight per matched term) / (n_primary_terms × weight_direct), clamped to 1.0. Primary term IDs (keys of htax_includes) use weight_direct (default 1.0); included term IDs (values of htax_includes) use weight_included_direct (default 0.7). Hidden-term rows excluded via is_hidden = 0. AND mode (htax_match=all) removes posts missing a match in any primary term group before normalization.
External Posts Platform (includes/core/external-posts.php)
Owned by TermConnect. See the TermConnect documentation.
Feed Platform (includes/core/feed-platform.php)
Owned by TermConnect. See the TermConnect documentation.
External Post List Shortcodes (includes/shortcodes/class-external-post-list.php)
Owned by TermConnect. See the TermConnect documentation.
Admin — Term Edit & Bulk Actions
Term Edit (includes/admin/term-edit.php)
admin/js/htax-term-edit.js — unsaved changes guard: On DOM-ready, captures $('#edittag').serialize() as the baseline form state. A beforeunload listener compares the current serialized state to the baseline and fires the browser’s native “Leave site?” dialog (via e.preventDefault() + non-empty e.returnValue) if they differ. The listener removes itself on form#edittag submit so the warning is suppressed when the user intentionally clicks Save. The baseline is also reset on htax:saved (AJAX save, no page reload). The comparison covers all standard inputs plus the group-checklist checkboxes (name="htax_groups[]") which are part of #edittag even when the floating dropdown is visually detached.
htax_enqueue_admin_assets()- Enqueue CSS/JS and localize data for term edit screens; injectsHTAX_Ajaxwithnonce(htax-autocomplete),link_suggest_nonce(htax-link-suggest),taxonomy(current taxonomy slug), visibility data, synonym data, and (for edit form)group_termsmap (groupId → [{id, name}] member list) andgroup_namesmap (groupId → name string for tooltip rendering of dynamically created indirect pills)htax_add_description_display_field()- Archive description checkbox fieldhtax_add_hidden_term_field()- Hidden term checkbox fieldhtax_is_add_form()- Helper to detect if form is add vs edit (true when$term_or_taxonomyis a string)htax_extract_term_edit_context( $term_or_taxonomy ): array- Normalizes a term-form renderer’s argument to{taxonomy, term_id, term}(term_id0 andtermnull on the add form; the taxonomy comes from the term on the edit form). Used by the description, hidden, redirect, archive settings, term groups and relationships field renderershtax_add_inclusions_tabs_field()- Unified tabbed inclusions/relationships field for the edit form; renders core inclusion tabs (Included Terms, Container Terms) and one tab per visible custom relationship type. Render uses low-level split caches (htax_get_direct_rel_term_ids,htax_get_direct_rel_group_ids,htax_get_reverse_rel_term_ids,htax_get_reverse_rel_group_ids) so group pills and direct-term pills are separately identifiable for editable vs read-only display; the group-expandedhtax_crel_caches are not used here. The save handler reads the same low-level split caches for diff computation.-
htax_render_merged_pill_container( array $args ): void- Render a single merged pill container. Named$argskeys:term_id,taxonomy,field_terms(hidden input name for direct term IDs),field_groups(hidden input name for group IDs; omit to suppress the groups section),direct_term_ids(int[]),group_ids(int[]),via_group_map(term_id → [group_id, …] indirect pills via group membership),via_container_map(term_id → [group_id, …] indirect pills via container-group attribution),custom_type_paths(term_id → [[label, dir, slug], …] for cross-panel attribution badges),redirected_term_ids(int[] — adds ↩ indicator),excluded_ids(int[] — suppresses indirect pills; addshtax-overriddenstrikethrough class to direct pills),panel(‘includes’‘containers’ ‘custom’, derived from field_termsif omitted),placeholder(search input text),disabled(bool),taxonomies(extra taxonomy slugs for cross-taxonomy relationships). Batch-loads all term and group objects via twoget_terms()calls, and all indirect-group objects via a third call, building$all_group_name_mapinline — group names for tooltip data are resolved entirely in PHP (not viaHTAX_Ajax.group_names, which is only used by JS for dynamically added pills). Outputs a.htax-tag-container.htax-merged-containerdiv withdata-panel,data-current,data-taxonomy,data-taxonomies,data-actionattributes containing: group section label + editable group pills + add-group button + terms section label + direct term pills (sorted A–Z, with × remove button) + add-term button +.htax-search.htax-merged-searchinput + hidden inputs + indirect section label + indirect pills (sorted A–Z,.htax-indirect, no remove button) + group dropdown checklist. JS-side:isTermExcluded()checks for an excluded pill by searching from.htax-rel-tab-panel(the common ancestor of the includes and excludes panels) — not from[data-panel]on the merged container itself, which is a descendant of the panel rather than a sibling to.htax-exclude-panel. htax_build_via_group_map( array $group_ids, string $taxonomy ): array- Callshtax_get_terms_in_groups( [$gid], $taxonomy )per group ID (uses its own per-group cache) and buildsterm_id => [group_id, ...]map. Per-group iteration preserves attribution; cache hits are cheap for groups already resolved this request.htax_build_container_via_group_map( array $cont_group_ids, string $taxonomy ): array- Expands$cont_group_idsupward viahtax_get_group_ancestors()(mirrors htax_get_container_terms() Path 3), then queriesmember_ofrowsWHERE target_term_id IN ($expanded)directly; returnsterm_id => [group_id, ...]. Used for the containers panel to find terms that are members of ancestor groups of the current term’s container groups.- Hide indirect relationships toggle: Eye-icon
<button class="htax-via-header-btn htax-via-section-toggle">rendered in the Term Relationships<th>header when any indirect pills exist for the current term. Clicking it toggles class.htax-via-hiddenon.htax-inclusions-fieldvia JS; CSS hides all.htax-indirectpills when that class is present. State persisted per term inlocalStorageunder keyhtax-via-hidden-section-{termId}. htax_add_redirect_field()- Term redirect fieldhtax_add_archive_settings_field()- Archive customization settings field (sticky posts, posts per page, sort order)htax_handle_add_custom_relationship_from_form()- Handle inline add-row form submission for custom relationship types (used in add form context)
Term Edit Locking (includes/admin/term-edit-lock.php)
Prevents concurrent editing of the same term or relationship type by multiple admins, using the same mechanism as WordPress post locking: a heartbeat-refreshed lock with a take-over flow.
Lock storage:
- Terms:
_htax_edit_lockterm meta —"{timestamp}:{user_id}". Applies to every term edit screen (edit-tags.php/term.php) regardless of whether the taxonomy is HyperTaxonomy-enabled; covers groups, custom taxonomies, and built-in taxonomies. - Relationship types:
htax_rt_lock_{slug}transient — same format; TTL =HTAX_LOCK_TIMEOUT + 30sso transient expiry slightly outlasts the logical timeout.
Lock timeout: HTAX_LOCK_TIMEOUT = 150 seconds (matches WordPress post lock). A lock with no heartbeat refresh for 150 s is considered expired.
Page-load flow: On load-edit-tags.php / load-term.php (terms) or admin_init when ?page=htax-term-relationships&edit=slug (RT): if the lock belongs to another non-expired user, render the blocking #htax-term-lock-dialog overlay with “Go Back” and “Take Over” buttons; otherwise set the lock and render the edit form normally. The take-over button is a nonce-protected URL (?htax-take-lock=1 / ?htax-rt-take-lock=1); the handler verifies the nonce, forcibly overwrites the lock, then redirects back to the same URL without the parameter.
Heartbeat flow: The client (admin/js/htax-term-edit-lock.js) sends htax_term_lock_refresh or htax_rt_lock_refresh on each heartbeat tick. The server handler (htax_heartbeat_refresh_lock) verifies the nonce, then either refreshes the lock timestamp (returns lock_refreshed: true) or signals displacement (returns lock_error: true with the new holder’s name and avatar URL). On displacement the JS hides the editing section, populates the #htax-lock-message-taken section, and shows the dialog.
Dialog HTML: Always rendered in admin_footer via htax_render_lock_dialog() using WordPress’s standard .notification-dialog-wrap CSS classes. Contains two sections: #htax-lock-message-editing (shown at page load when another user holds the lock) and #htax-lock-message-taken (always hidden at load, shown by JS on heartbeat displacement). When the current user holds the lock the entire dialog is hidden at page load.
htax_set_term_edit_lock( int $term_id )- Write or refresh_htax_edit_lockterm meta for current userhtax_check_term_edit_lock( int $term_id ): WP_User|false- Return locking user or false (own lock / expired / absent)htax_set_rt_edit_lock( string $slug )- Write or refreshhtax_rt_lock_{slug}transienthtax_check_rt_edit_lock( string $slug ): WP_User|false- Same semantics as term varianthtax_heartbeat_refresh_lock( array $response, array $data ): array-heartbeat_receivedfilter; handles bothhtax_term_lock_refreshandhtax_rt_lock_refreshpayloadshtax_enqueue_lock_script( array $config )- Enqueuehtax-term-edit-lockscript and passHTAXLockconfig (term_id or rt_slug, nonce, locked-user data, list URL, i18n strings)htax_render_lock_dialog( WP_User|false $locked_by, string $back_url, string $take_url )- Output the dialog HTML; both sections always present, visibility controlled by PHP for initial state and JS for displacementhtax_term_lock_on_load()-load-edit-tags.php/load-term.phphandler; skips add screens (no tag_ID); handles take-over redirect; sets up enqueue and footer render closureshtax_rt_lock_on_admin_init()-admin_inithandler for relationship type edit page; same flow as term variant
Bulk Actions (includes/admin/bulk-actions.php)
htax_add_bulk_actions( array $bulk_actions ): array- Filter callback onbulk_actions-edit-{$taxonomy}; appendshtax_make_visible,htax_make_hidden(when visibility enabled),htax_remove_icon(when icons enabled), andhtax_assign_to_groups(when group assignment enabled) to the term list table’s bulk action dropdownhtax_handle_bulk_actions( string $redirect_to, string $action, int[] $term_ids ): string- Filter callback onhandle_bulk_actions-edit-{$taxonomy}; processeshtax_make_visible,htax_make_hidden, andhtax_remove_iconfor each selected term ID;htax_assign_to_groupsis handled entirely via AJAX modal and returns early here; appendshtax_bulk_actionandhtax_processedquery vars to$redirect_towhen any terms were updated, for admin notice display. Forhtax_make_visible: silently skips any term whose ancestor is still hidden at processing time (a term cannot be made visible while a parent is hidden); because WP processes selected terms in list order, selecting both a parent and its descendant works correctly — parent is made visible first, descendant passes the ancestor check on its own turn.htax_ajax_bulk_assign_groups()—wp_ajax_htax_bulk_assign_groupshandler; acceptsterm_ids[],add_groups[],remove_groups[],taxonomy,nonce; for each term, merges its existing group IDs withadd_groupsthen removesremove_groups(preserving all unmentioned groups), then callshtax_set_term_groups(); returns{ processed: N, message: string }. The JS modal uses three-state checkboxes (checked = add to all, unchecked = remove from all, indeterminate = no change); only groups whose state differs from the initial state are sent asadd_groupsorremove_groups. Group labels passed to JS are decoded withhtml_entity_decode()beforewp_localize_scriptto avoid double-encoding (WordPress’sget_terms()already HTML-encodes names for display).
Admin — Group Admin (includes/admin/group-admin.php)
Group admin hooks to edited_group (and group_edit_form_fields) to manage four relationship sets visible from the group’s edit screen:
htax_add_terms_in_group_field()/htax_save_terms_in_group()— Display/savemember_ofrows (T→G membership). Save path: deleteshtax_grp_{group_id}_*caches (all taxonomies) viahtax_delete_transients_by_prefix, callshtax_reload_term_caches_on_editfor each changed member term, callshtax_invalidate_group_relationship_caches, then fireshtax_term_groups_changedfor search-index listeners.htax_add_terms_with_container_group_field()/htax_save_terms_with_container_group()— Display/saveincludesrows where this group is the source (G→T). Save path: callshtax_reload_term_caches_on_editfor each changed target term, callshtax_invalidate_group_relationship_caches, then callshtax_clear_member_caches_for_groups([group_id], 'inc')to clearhtax_inc_Mfor all members of this group and its ancestor groups (path 3 ofhtax_get_included_termsexpands member groups downward, so ancestor-group members also reach these container rows).htax_add_terms_with_included_group_field()/htax_save_terms_with_included_group()— Display/saveincludesrows where this group is the target (T→G: terms whose archives expand to include all this group’s members). Save path: callshtax_reload_term_caches_on_editfor each changed source term, callshtax_invalidate_group_relationship_caches, then callshtax_clear_member_caches_for_groups([group_id], 'cont')to clearhtax_cont_Mfor all members of this group and its descendant groups (htax_get_container_termspath 2 matches exact membership;htax_get_terms_in_groupsexpands downward).htax_add_group_custom_relationships_field()/htax_save_group_custom_type_relationships()— Display/save custom (non-core) relationship rows where this group is source or target. Renders one tab per registered non-core type, each with a forward panel (G→U, group as source) and a reverse panel (U→G, group as target). Groups cannot be on the other side because group+group rows are always invalid, so only non-group term IDs are stored. Save path: diffs submittedhtax_cust_{slug}_fwd_terms/htax_cust_{slug}_rev_termsagainst current DB rows and callshtax_add_term_relationship/htax_remove_term_relationshipfor each change (those functions handle cache invalidation).
Terminology: “terms with container group” (G→T) and “terms with included group” (T→G) are opposite directions of the includes relationship involving a group. htax_grp_* caches are only affected by membership changes (member_of rows). G→T container-group changes affect htax_inc_* caches for the target term T and for members M of the group (and ancestor groups). T→G included-group changes affect htax_cont_* caches for members M of the group (and descendant groups). Custom-type group rows additionally affect htax_crel_*/htax_crel_rev_* caches for members. htax_invalidate_group_relationship_caches handles the T and custom-rel side; htax_clear_member_caches_for_groups handles the member side.
htax_render_group_term_section()- Shared pill-container renderer used by the member/container/included-group fields (per-taxonomy sections)htax_render_group_crel_pill_container()- Cross-taxonomy pill container used by the custom relationships field; loads term objects across all enabled non-group taxonomies (term IDs are globally unique); useshtax_search_termsAJAX action withdata-taxonomiesscoped to the type’starget_taxonomies/source_taxonomies(or all enabled non-group taxonomies if unset)htax_get_taxonomy_map_for_terms( int[] $term_ids )- Batch-fetch taxonomy slugs for term IDs; used by save functions to build source_taxonomy values for new rows
Admin — Relationship Types (includes/admin/relationship-types-admin.php)
Admin UI for viewing built-in relationship types and managing user-defined types. Registered as the “Term Relationship Types” submenu under the Terms top-level menu.
htax_render_term_relationships_page()- Render the Term Relationships admin page; shows built-in types as a read-only table and custom types with edit/delete actions; “New Inclusion-based” and “New Custom” buttons open the#htax-rt-panefloating dialog (same as Edit) with a slug input and cleared fields; Edit and Delete actions also operate through the pane/AJAX without page navigationhtax_handle_save_relationship_type()-admin_post_htax_save_relationship_typehandler; validates and persists a new or updated custom type viahtax_set_custom_relationship_type(); callshtax_remove_invalid_term_relationships()after save to purge rows that violate the updated constraintshtax_handle_remove_relationship_type()-admin_post_htax_remove_relationship_typehandler; deletes a custom type viahtax_remove_custom_relationship_type(); redirects back with success/error noticehtax_ajax_count_ineligible_custom_relationships()-wp_ajax_htax_count_ineligible_custom_relationshipshandler; counts existing relationship rows that would become invalid if taxonomy constraints are narrowed; used by the edit form to warn before savinghtax_render_taxonomy_checkboxes( string $field_name, array $selected, array $taxonomies )- Render source/target taxonomy checkbox inputs for the add/edit forms
Admin — Link Types (includes/admin/link-types-admin.php)
Owned by TermConnect. See the TermConnect documentation.
Admin — Tables, AJAX, Import/Export
Post Editor (includes/admin/post-edit.php) Loaded unconditionally (outside is_admin()) so the REST field is available during REST API requests.
htax_enqueue_post_editor_assets()- Enqueue CSS/JS and build JS globals for the block/classic editor. PHP always emits the full taxonomy-gated data; per-user display preferences (which of the five indicators to show) are stored in the Gutenbergcore/preferencesstore underhtax/post-editand applied client-side. Also enqueueshtax-editor-prefs.jsin the block editor, which registers fivePluginMoreMenuItemtoggles in the ··· menu and dispatches a one-time Gutenberg notice (viacore/notices) on first block-editor visit; dismiss state stored in user meta_htax_prefs_callout_dismissed(set viahtax_ajax_dismiss_prefs_callout/wp_ajax_htax_dismiss_prefs_callout). Globals emitted:htaxTermMeta— keyed by term ID; each entry:{id, name, slug, taxonomy, is_redirect, is_hidden, is_hierarchical, inclusion_count, container_count, included_names, container_names, redirect_target_name}. Hierarchical taxonomies include all terms; flat taxonomies include only the post’s currently assigned terms. Terms with at least one HT flag are always included; additionally, all assigned terms of acustomize_archives-enabled taxonomy are included (even plain terms with no flags) so the sticky pin can be shown.htaxPreloadedTerms(Gutenberg only, when apostURL param is present) — assigned term objects injected into the Gutenberg Redux store beforewp-edit-postinitializes, viawp_add_inline_script('wp-edit-post', ..., 'before')usingreceiveEntityRecords+finishResolution. Eliminates the per-term REST calls Gutenberg would otherwise make when it resolvesgetEntityRecord.htaxFlatTaxonomies(Gutenberg only) — array of{slug, restBase}for flat (non-hierarchical) HT-enabled taxonomies that are REST-exposed. Used by the JS autocomplete subscriber.htaxTaxonomyFeatures(Gutenberg only) —{[taxonomy]: {inclusions, containers, redirects, visibility}}per-taxonomy feature flags (fromhtax_taxonomy_has_feature()). Applied client-side byfetchPendingMetawhen merging REST responses. User display preferences are NOT reflected here — they come fromcore/preferences.htaxStickyConfig(Gutenberg only) — array of{name, label, restBase}for taxonomies with thecustomize_archivesfeature; always emitted when at least one eligible taxonomy exists. Whether pins actually appear is gated by the per-userhtax/post-edit → stickypreference. Badge rendering (viaadmin/js/htax-post-edit.jsandadmin/css/htax-post-edit.css): Classic Editor injects badge<span>elements before term labels; Gutenberg token pills receive badge groups prepended intotoken-text, while hierarchical checklist rows receive badge groups injected between the checkbox and label. Deduplication uses adata-htax-badgessignature attribute — the DOM is only touched when counts or state change. Badge groups display:- Includes badge:
⇉ [N](rightward-double-arrow symbol with count) in light blue (#ddeeffbg,#7ab8e8border,#003366text) - Container badge:
⇇ [N](leftward-double-arrow symbol with count) in light green (#eef7efbg,#a5d6a7border,#2e7d32text) - Redirect indicator: Term names receive italic styling, plus a
↪hook symbol (U+21AA) appended to the name via.redirect-indicatorspan; tooltip shows redirect target name - Hidden indicator: Terms marked hidden receive reduced opacity (45%) via
data-htax-hidden="1"attribute selector Autocomplete subscriber (Gutenberg, flat taxonomies only):wp.data.subscribewatches the Redux store for term IDs newly added via the Gutenberg tag autocomplete mid-session (i.e. IDs not present inhtaxTermMetaat page load). When a new ID is detected, it callsGET /wp-json/htax/v1/term-metato fetch badge metadata and merges the result intohtaxTermMetafor subsequent badge rendering passes. Terms with no HT flags are merged when the taxonomy hascustomize_archivesenabled (so newly-added plain tags still get sticky pins). Sticky for Terms panel:useEffectauto-removes terms from thehtax_sticky_termspost attribute whenever they are unassigned from the post. Checkboxes for newly-added (unsaved) terms are disabled with the helper text “Save the post first to enable sticky for this term.” Redirect terms are excluded from sticky options entirely (their archives redirect away, so stickying has no visible effect).
htax_register_sticky_rest_field()- Registerhtax_sticky_termsREST field on all eligible public post types (viarest_api_init)htax_rest_get_sticky_terms()- REST GET callback: return sticky term IDs keyed by taxonomy slughtax_rest_update_sticky_terms()- REST UPDATE callback: write/delete_htax_sticky_{term_id}post meta and invalidate cacheshtax_get_sticky_terms_by_taxonomy( int $post_id )- Return a post’s sticky term IDs grouped by taxonomy slug; shared by the REST GET callback and the update callback’s change detection (called with a post ID only — no$requestneeded)
List Table (includes/admin/tables.php)
htax_enqueue_table_assets()- Enqueue list table CSS/JS with localized filter config (htaxTablesConfig); includesbulkAssignNonceandgroupFilter.options(group names decoded viahtml_entity_decode()to avoid double-encoding fromget_terms()display escaping); localized config powers both the Term Groups filter dropdown and the bulk assignment modalhtax_add_icon_column()- Add icon column to term list tables (AJAX-compatible)htax_render_icon_column()- Render icon column contenthtax_add_hidden_column()- Add visibility column to term list tables (AJAX-compatible)htax_display_hidden_column()- Display visibility icons in term lists; also renders a hidden<span class="htax-qe-vis-data" data-hidden="0|1" data-parent-hidden="0|1">per row.data-hiddenreflects the term’s own visibility state;data-parent-hiddenis 1 when any ancestor is currently hidden (computed by walking the parent chain againsthtax_get_all_hidden_term_ids(), cached with a static variable across rows). Quick Edit JS readsdata-parent-hiddento disable the visibility checkbox with a tooltip when the user cannot change visibility without first making a parent visible.htax_quick_edit_fields()- Renders the HyperTaxonomy fieldset inside the term Quick Edit row (hooked onquick_edit_custom_box); anchors tohtax_term_groupscolumn when group_assignment is enabled, otherwise tohtax_hidden; renders once per row. Values are populated from per-row data spans byhtax-tables.jsoninlineEditTax.edit. Saving reusesedited_{taxonomy}hooks (htax_save_term_data,htax_save_term_groups), which accept WP’s Quick Edit nonce (_inline_edit/taxinlineeditnonce).htax_ajax_count_hidden_descendants()—wp_ajax_htax_count_hidden_descendantshandler; acceptsterm_ids[],taxonomy,nonce; for each term ID returns a count of hidden descendants viaget_term_children()filtered againsthtax_get_all_hidden_term_ids(). Descendants that are themselves present interm_idsare excluded from every count — when a parent and child are both selected for bulk “Make Visible”, the child does not inflate the cascade-prompt count since it will be made visible on its own. Called before bulk and Quick Edit make-visible saves to decide whether to show the cascade modal.htax_add_redirect_column()- Add redirect column to term list tables (AJAX-compatible)htax_display_redirect_column()- Display redirect information (count of redirecting terms)htax_add_term_groups_column()- Add Term Groups column to term list tables (AJAX-compatible)htax_term_groups_column_content()- Render Term Groups badges and a hidden<span class="htax-qe-grp-data" data-group-ids="...">with comma-separated group IDs; this span is read by the bulk assignment modal JS to compute initial three-state checkbox values without an extra AJAX round-triphtax_register_sortable_columns( $columns )- Registers Redirect, Icon, Visibility, and Term Groups columns as sortable onmanage_edit-{taxonomy}_sortable_columns; guards each column behind its corresponding feature flaghtax_sortable_column_terms_clauses( $clauses, $taxonomies, $_args )-terms_clausesfilter; rewritesORDER BYwhen?orderby=matches a HyperTaxonomy column key; scoped tohtax_is_term_list_request()only; usesEXISTSsubqueries (redirect row inwp_htax_term_relationships, meta presence inwp_termmeta) so all terms appear regardless of whether they carry the attribute; secondary sort alwayst.name ASC; Term Groups usesGROUP_CONCATof group names to produce a stable alphabetic keyhtax_tag_row_class()- Add CSS class to redirect term rowshtax_add_sticky_indicators()- Add sticky indicators to term columns in post list
Term Filters (includes/admin/term-filters.php)
htax_filter_terms_by_redirect()-get_terms_argsfilter; narrows term queries to redirect or non-redirect terms when the admin list filter is active. Filter values:standard(Non-Redirects — excludes redirect sources, includes redirect targets),redirects,auto_redirects,targets(Redirect Targets only).htax_is_term_list_request(): bool- Returns true when the current request is an admin term list screen or its AJAX counterpart (Quick Edit). Used to gateget_terms_argsfilters that should only apply on list screens, not on dropdowns or autocomplete queries.htax_merge_term_ids_to_exclude( array &$args, int[] $ids ): void- Merge$idsinto$args['exclude'], deduplicating and removing any IDs that also appear in$args['include'](WordPress’sget_termsignores exclude when the same ID is included).htax_intersect_term_ids_to_include( array &$args, int[] $ids ): void- Restrict results to$idsby setting$args['include']; if$args['include']is already set, takes the intersection so existing include constraints are preserved.htax_push_meta_query_clause( array &$args, array $clause ): void- Append ameta_queryclause to$args, initialising themeta_querykey as needed.
AJAX Handlers (includes/admin/ajax.php)
htax_ajax_get_group_terms()- Get terms from Term Groupshtax_ajax_search_terms()- Autocomplete search (6-bucket ranking); acceptstaxonomies[], optionalinclude_groups/exclude_groups(comma-separated group IDs expanded to term IDs viahtax_get_terms_in_groups()to restrict the option pool); results cached viahtax_get_cache_duration('autocomplete')htax_ajax_get_group_term_ids()(wp_ajax_htax_get_group_term_ids, alsonopriv) — Returns all term IDs belonging to the givengroups(comma-separated) fortaxonomy. Rate-limited; nonce required for admin users. Used by the search widget JS to determine which selected pills to animate-remove when a group is excluded from the option pool.-
htax_ajax_import_tag_groups_init()(wp_ajax_htax_import_tag_groups_init) - Chunked import, first request: resolves/creates/renames groups, applies hierarchy, detects deletions. Acceptsmode(mirrorkeep) for this run only; the choice is chosen per run in the import modal and not persisted. htax_ajax_import_tag_groups_batch()(wp_ajax_htax_import_tag_groups_batch) - Chunked import, batch request: assigns one batch of terms. Acceptsgroup_mapping,offset,batch_size,mode.htax_ajax_import_tag_groups_finish()(wp_ajax_htax_import_tag_groups_finish) - Chunked import, final request: clears caches, stores the run snapshot viahtax_store_tag_groups_import_snapshot().htax_ajax_dismiss_tag_groups_notice()(wp_ajax_htax_dismiss_tag_groups_notice) - Permanently dismisses the Tag Groups coexistence notice for the current user (_htax_dismissed_tag_groups_noticeuser meta).htax_clear_tag_groups_notice_dismissal_on_deactivate( $plugin )(deactivated_plugin) - Clears every user’s_htax_dismissed_tag_groups_noticemeta when a plugin whose basename starts withtag-groupsis deactivated, so the coexistence notice is shown again (until re-dismissed) if Tag Groups is reactivated later.htax_ajax_export()- Export HyperTaxonomy data to JSONhtax_ajax_preview_import()- Preview HyperTaxonomy import datahtax_ajax_import()- Import HyperTaxonomy data from JSONhtax_find_after_non_letter()- Find search after word boundaryhtax_highlight_match()- Highlight search matches
Export/Import (includes/admin/)
Export (export-hypertaxonomy.php):
htax_generate_export_data( $taxonomies, $include_term_posts )- Generate JSON export data- Exports terms with metadata (visibility, icons, archive settings, sticky posts)
- Exports relationships (includes, containers, redirects - all using slug-based matching)
- Optional: exports term-to-post assignments
- Returns array with version, site_url, exported_at, terms[], relationships[], term_post_assignments[]
htax_download_export( $taxonomies, $include_term_posts )- Trigger JSON file download
Import (import-hypertaxonomy.php):
htax_validate_import_data( $data )- Validate JSON structure and versionhtax_preview_import( $data )- Analyze import file and return preview summaryhtax_process_import( $data, $options )- Core import logic (3 phases: terms, relationships, post assignments)htax_process_import_with_output( $data, $options )- Import with HTML progress outputhtax_import_icon_from_url( $url )- Match icons by filename from URL
Tag Groups Migration (import-tag-groups-handler.php): Tag Groups was rebranded “PublishPress Tag Groups” in October 2026 — the database format below is unchanged under either name.
htax_get_tag_groups_site_token(): string- Random UUID, generated once and stored in thehtax_tag_groups_site_tokenoption, scoping the mapping below to this site. Confirmedhtax_generate_export_data()’s$meta_keysis a hardcoded enumerated whitelist that does not include the mapping meta key, so HT’s own JSON export/import never carries it across sites — the scoping instead guards against other wayswp_termmetacan move between sites (a raw database export/import, a staging→production clone, a third-party site-migration plugin), where a bare Tag Groups ID could otherwise collide with an unrelated group on the destination site and silently rename it.htax_make_tag_groups_source_key( int $tg_group_id ): string/htax_parse_tag_groups_source_key( string $key ): ?int- Build/parse the{site_token}:{tg_group_id}mapping key; parse returnsnull(not a wrong match) for a key stamped on another site.htax_resolve_or_create_tag_group( int $tg_group_id, string $label, ?string $parent_label, array &$stats ): int- ID-match (this site’s mapping) → name-match against unmapped groups only (first import, or re-linking after a cross-site meta mismatch) → create. The HT name comes fromhtax_get_tag_groups_display_name(), so it is the plain label unless another group with that name is mapped to a different Tag Groups group; then it is suffixed (see helpers below). Keeps the name in sync on every run, so Tag Groups-side renames propagate instead of creating a duplicate. Name comparison decodes HTML entities on both sides, so “&” in a Tag Groups label matches “&” stored in HyperTaxonomy and does not trigger a rename on every run. Records newly-stamped mappings (name-match and create cases) to$stats['newly_mapped'], the signalhtax_import_tag_groups_hierarchy()uses to seed a parent field once and never again.htax_orphan_deleted_tag_groups( array $current_tg_group_ids ): array- Strips the mapping meta (never deletes the HT term) for any previously-mapped group no longer in Tag Groups’ currentterm_groupsoption.htax_import_tag_groups_hierarchy( array $group_mapping, array $newly_mapped_old_ids ): array- Seeds Tag Groups Pro’s parent-group data (term_group_positions+term_group_parents) onto the nativeparentfield of newly-mapped HT group terms only —$newly_mapped_old_idsrestricts writes to groups mapped for the first time this run; a group already mapped from a prior run is never touched again, deliberately. (HT’s native hierarchy supports arbitrary depth; Tag Groups Pro’s is capped at two levels — re-applying on every run would silently flatten any deeper structure an admin built in HT back down to that cap.) Single pass, not recursive — Tag Groups’ own documentation confirms parent groups are capped at two levels. Provenance-aware even for the one-time seed: skips (and counts as a conflict) rather than overwrites when a newly-matched group’s current parent is a non-mapped, HT-native group.htax_split_term_groups_by_provenance( array $group_ids ): array- Splits a term’s current HT groups into Tag-Groups-mapped vs. HT-only, by presence of the mapping meta.htax_apply_tag_groups_term_assignment( int $term_id, string $taxonomy, array $tg_mapped_new_ids, string $mode = 'mirror' ): string- Applies one term’s Tag Groups membership report to its HT group assignment (the provenance-filtered merge), returning'assigned','skipped', or'error'. Factored out of the batch loop so it’s independently callable — seetag-groups-live-sync-coexistence.md.htax_store_tag_groups_import_snapshot(): void- Stores{groups, terms, time}to thehtax_tag_groups_last_importoption after a successful run, for the migration screen’s diff display.htax_import_tag_groups_create_groups(): array- First pass: resolves/creates/renames group terms, applies hierarchy, detects deletions. No longer matches by name only — ID-first via the site-scoped mapping.htax_import_tag_groups_process_batch( array $group_mapping, int $offset, int $batch_size, string $mode = 'mirror' ): array- Batch pass: assigns one batch of terms to their groups. No longer restricted topost_tag/category— any taxonomy works, gated the same way the per-rowgroup_assignmentfeature check already did.$modecontrols the Tag-Groups-mapped subset only (HT-only memberships are always preserved):'mirror'(default) makes it identical to Tag Groups’ current report for the term, including removals;'keep'only ever adds.- Reads from wp_options: term_groups, term_group_labels, term_group_positions, term_group_parents
- Reads from wp_termmeta: _cm_term_group_array
- Converts to HyperTaxonomy Term Groups
Integration mode (import-tag-groups-handler.php):
htax_tag_groups_integration_mode(): string- Thehtax_tag_groups_integration_modeoption ('independent'or'locked'; default'independent'), validated against the known set.'independent'disables every lock below regardless of what data Tag Groups has written;'locked'locks them between manual migration runs. Set from Settings → HyperTaxonomy → Export / Import → Sync via thehtax_save_tag_groups_sync_settingsAJAX action (ajax.php), which saves the mode, the continuous-sync checkbox, and removal handling (htax_tag_groups_sync_mode) together. Enabling continuous sync reportsneeds_resync: trueso the UI can run one full resync first via the Migrate subtab (resync=1).
Re-run overwrite protection (import-tag-groups-handler.php, loaded unconditionally in admin — not just from the importer’s own AJAX requests, since htax_save_term_groups()/htax_save_terms_in_group() need these on every term/group save):
htax_tag_groups_import_in_progress( ?bool $set = null ): bool- Reentrancy flag, set aroundhtax_import_tag_groups_create_groups()’s own rename/hierarchy writes sohtax_lock_tag_groups_mapped_name()can tell the importer’s own legitimate write apart from an admin’s.htax_tag_groups_taxonomy_is_managed( string $taxonomy ): bool- Whether Tag Groups has actually written_cm_term_group_arraytermmeta for a taxonomy, read directly rather than via any Tag Groups setting — the gate for every lock below. Returnsfalseunconditionally when integration mode is'independent'. Per-request cached.htax_lock_tag_groups_mapped_name( $data, $term_id, $taxonomy, $args )- Filterswp_update_term_data(WP core, fires inwp_update_term()before the DB write, unconditionally for every entry point — full edit screen, native Quick Edit, REST, WP-CLI). For agroupterm carrying_htax_tag_groups_source_id, outside an import pass and outside'independent'mode, restores$data['name']to the term’s current name so a rename never sticks through any path except the importer itself.htax_save_term_groups( int $term_id )(term-edit.php) - Unchanged signature; now re-merges in the Tag-Groups-mapped subset of the term’s existing group IDs over whatever was submitted, for any taxonomyhtax_tag_groups_taxonomy_is_managed()is true for. Covers the term’s own edit screen and Quick Edit’s “Term Groups” checklist, which reuses this handler.htax_save_terms_in_group( int $term_id )(group-admin.php) - Unchanged signature; for a group carrying_htax_tag_groups_source_id, carries forward the current members of any locked taxonomy instead of applying the submission for it (per-taxonomy, since one group can hold members across several at once).htax_get_tag_groups_parent_map(): array- Child Tag Groups group ID → parent group ID, derived fromterm_group_positionsandterm_group_parents(single pass; Tag Groups Pro caps hierarchy at two levels). Used by the hierarchy seed and to look up parent labels for naming.htax_find_group_terms_by_label( string $label ): WP_Term[]- HT group terms whose decoded name equals$label.htax_get_tag_groups_display_name( string $label, ?string $parent_label, string $source_key, int $own_term_id = 0 ): string- Returns$labelunless another HT group with that name is mapped to a different Tag Groups group. A clash appends the parent label asLabel (Parent), falling back toLabel (2),Label (3)… when there is no parent or the candidate is taken.$own_term_idis ignored when checking whether a candidate is taken, so a group keeps its own suffix on later runs.htax_is_group_label_taken( string $label, int $own_term_id ): bool- Whether any HT group other than$own_term_idhas this name.htax_tag_groups_native_field_promoted(): bool- Whether Tag Groups’ own native per-term field should take the always-visible spot HT’s Term Groups selector normally occupies on the term edit/add screens, with HT’s own (now read-only) selector demoted into the “More options” collapsible instead. True only when Locked mode is selected and Tag Groups is currently active. Read byhtax_add_edit_form_collapsible_start()/htax_add_form_collapsible_start()(emitted asdata-promote-native-groupsfor the JS reorder in htax-term-edit.js / htax-tables.js) and by the{taxonomy}_*_form_fieldsregistration inincludes/hooks/actions.php(controls whetherhtax_add_term_groups_field()is hooked at priority 8, always visible, or priority 16, inside the collapsible between Redirect and Term Relationships).htax_find_ht_group_by_tag_groups_id( int $tg_group_id ): ?int- Read-only companion tohtax_resolve_or_create_tag_group()(which now calls this internally for its own ID-match step) — looks up the HT group mapped to a Tag Groups ID on this site without creating anything. Used by the live-sync membership handler, which only ever sees IDs Tag Groups itself reports as already existing.
Live sync (tag-groups-live-sync.php, loaded unconditionally in admin alongside import-tag-groups-handler.php; registers nothing unless htax_tag_groups_live_sync_active() is true):
htax_tag_groups_live_sync_enabled(): bool(import-tag-groups-handler.php) - Thehtax_tag_groups_live_syncoption is on and the integration mode is'locked'. It is cleared whenever the mode is saved as'independent'.htax_tag_groups_live_sync_active(): bool(tag-groups-live-sync.php) -function_exists('tag_groups_init')andhtax_tag_groups_live_sync_enabled().htax_tag_groups_live_sync_register()(tag-groups-live-sync.php) - Hooked onplugins_loaded(priority 20) and registers the three Tag Groups hooks below only whenhtax_tag_groups_live_sync_active()is true. Runs after every plugin has loaded, so Tag Groups’ functions exist; checking at file-include time would miss Tag Groups when it loads after HyperTaxonomy. Changes take effect on the next page load, not dynamically mid-request.- Three Tag Groups hooks (Free v3.0.0 / Pro v2.0.9), registered only when the above is true:
term_group_saved(no args) →htax_tag_groups_live_sync_on_group_saved()- thin wrapper callinghtax_import_tag_groups_create_groups()directly; that function already does this hook’s whole job (resolve/create/rename, one-time hierarchy seed, orphan detection).tag_groups_term_group_deleted( int $group_id )→htax_tag_groups_live_sync_on_group_deleted()- callshtax_orphan_deleted_tag_groups()against Tag Groups’ currentterm_groupsoption.tag_groups_groups_of_term_saved( array $term_groups, int $term_id )→htax_tag_groups_live_sync_on_term_groups_saved()- maps Tag Groups’ IDs to HT IDs viahtax_find_ht_group_by_tag_groups_id()(skipping any not yet mapped) and callshtax_apply_tag_groups_term_assignment(), same as the periodic batch importer does per term.
UI niceties for the above (no new enforcement — the save-function guards above are what actually prevent the overwrite):
htax_hierarchical_checklist()(class-hierarchical-checklist.php) gaineddisabled_terms(int[]) anddisabled_title(string) args, and the walker’sstart_el()now accepts$args['disabled']as either a bool (existing all-items behavior) or an array of specific term IDs to disable — used to gray out locked groups in the term-edit screen’s add-group checklist.htax_render_group_term_section()(group-admin.php) gained a$locked_taxonomiesparameter (defaultarray()); onlyhtax_add_terms_in_group_field()(“Group Members”) passes a non-empty value — the other two callers (inclusion-type fields) are never locked.htax_group_tag_groups_mapping_notice( WP_Term $term ): void(group-admin.php) -group_edit_form_fieldsnotice on a mapped group’s own edit screen stating its locked fields and what deleting it does (not blocked, just surfaced).htax_group_custom_column_content()(group-admin.php) - the “Taxonomies” list-table column gets a lock-icon prefix for a mapped group, so it’s identifiable without opening it.- Quick Edit’s group checklist (
htax-tables.js, built fromhtaxTablesConfig.groupFilter) gainedlockedGroupIds/lockedTitle, localized from PHP once per screen (the lock is per-taxonomy, not per-row) — disables the matching checkboxes with the same tooltip.
Cache Management (includes/core/cache.php)
Nuclear (full-reset):
htax_get_transient( $key )/htax_set_transient( $key, $value, $expiration )/htax_delete_transient( $key )- Transient API wrappers used for every HyperTaxonomy transient; resolve the storage key viahtax_transient_key()htax_transient_key( string $key ): string- Under an external object cache, appends~{global generation}{family generation}tohtax_*keys; otherwise returns the key unchangedhtax_transient_family( string $key ): string- Maps a key to its invalidation family (rel,contrib,pl,total,ac,sticky,misc)htax_cache_generation( string $family, bool $bump = false ): string- Current (or new) random generation token for a family, stored in thehtax_cache_generationobject-cache group-
htax_bump_cache_family( string $family ): void- Called after every bulkDELETE … LIKE: bumps the family generation under an external object cache, otherwise flushes the runtimeoptionscache group htax_clear_caches_on_feature_change( $old_value, $new_value )-update_option_htax_settings(priority 10): callshtax_clear_all_caches()whentaxonomy_featureschanged. Relationship caches are feature-independent (the getters applyterm_relationshipson read), but rendered outputs — post-list results, contributing terms — are computed with the flags appliedhtax_clear_all_caches()- Nuclear option: deletes all_transient_htax_*entries. Per-type variants:htax_clear_relationship_caches(),htax_clear_search_caches()(htax_contrib_),htax_clear_autocomplete_caches()(htax_ac_),htax_clear_sticky_caches()(htax_sticky_),htax_clear_shortcode_caches()(htax_pl_, htax_total_*)
Per-term invalidation:
htax_delete_term_transients( int $term_id )- Delete all transients for a single term: core logical caches (htax_inc_,htax_cont_,htax_excl_,htax_redir_,htax_groups_,htax_syno_), contrib cache, all direct relationship caches for every registered type viahtax_delete_custom_rel_caches_for_term()(covershtax_crel_,htax_crel_rev_,htax_direct_rel_,htax_direct_rel_grp_for each slug including core'includes'), and post list shortcode caches. After clearing, callshtax_queue_term_cache_rebuild()to schedule async re-warming.htax_delete_term_transients_batch( array $term_ids )- Batch-delete core relationship transients for multiple terms in two queries instead of N×individual calls. Builds all named option keys forhtax_inc_,htax_excl_,htax_redir_,htax_groups_,htax_cont_, andhtax_syno_prefixes across all term IDs and removes them in a singleDELETE … WHERE option_name IN (…). Then does two broad prefix deletes forhtax_contrib_andhtax_pl_(one query each, covering all affected terms at once). Finally queues each term ID viahtax_queue_term_cache_rebuild(). Used byhtax_invalidate_group_relationship_caches()to handle large fan-outs efficiently.htax_queue_term_cache_rebuild( int $term_id )- Buffer a term ID in$GLOBALS['htax_term_rebuild_queue']. Registershtax_rebuild_queued_term_cacheson theshutdownhook (WordPress deduplicates same-callbackadd_actioncalls, so multiple invocations per request collapse to a single pass). WhenHTAX_TIMINGis enabled, emits aQUEUE_REBUILDtrace with a 5-frame call stack on the first enqueue of each term ID per request.htax_rebuild_queued_term_caches()- Shutdown callback (priority 5) that schedules an async cron event for cache re-warming. Collects all queued term IDs and callswp_schedule_single_event(time(), 'htax_async_rebuild_term_caches', [$queue]), then spawns WP-Cron non-blocking. The response is never delayed by this call; the actual rebuild runs in the next cron pass.htax_do_rebuild_term_caches( array $queue )- Async cron handler (hooked tohtax_async_rebuild_term_caches). Re-warmshtax_inc_{term_id},htax_cont_{term_id}, andhtax_syno_{term_id}for each queued term ID by callinghtax_get_included_terms(),htax_get_container_terms(), andhtax_get_synonym_terms(). Skips term IDs that no longer exist.htax_delete_custom_rel_caches_for_term( int $term_id )- Deletehtax_crel_{slug}_{term_id},htax_crel_rev_{slug}_{term_id},htax_direct_rel_{slug}_{term_id},htax_reverse_rel_{slug}_{term_id},htax_direct_rel_grp_{slug}_{term_id}, andhtax_reverse_rel_grp_{slug}_{term_id}for every registered relationship type; called byhtax_delete_term_transients()htax_invalidate_term_caches_on_delete( int|WP_Term $term, int $tt_id, string $taxonomy )- Full cache invalidation for a deleted term. Uses a read-before-clear pattern: reads T’s outgoing direct caches (htax_direct_rel_includes_T,htax_direct_rel_grp_includes_T,htax_direct_rel_{slug}_T, andhtax_direct_rel_grp_{slug}_Tfor all non-core slugs) before clearing T’s own transients. Steps: (1) PRE-CLEAR: if all those caches are warm, the outgoing data is fully available with no DB queries; if any are cold, falls back to a broadSELECT … WHERE source_term_id = Tquery and parses rows into categorised arrays ($out_inc_ids,$out_inc_grp_ids,$out_crel_ids[slug],$out_crel_grp_ids[slug]); (2) clears T’s own caches viahtax_delete_term_transients(T); (3) surgically updates member-group caches; (4a) for incoming references (A→T rows): callshtax_reload_term_caches_on_edit(A)— when A is a group (G→T row), also callshtax_clear_member_caches_for_groups([G], 'inc')to clearhtax_inc_Mfor members of G and ancestor groups; (4b) for outgoing references, iterates flat pre-read arrays:$out_inc_ids→ deleteshtax_cont_U+htax_reverse_rel_includes_U;$out_inc_grp_ids→ deletes G’s ownhtax_reverse_rel_includes_Gthen callshtax_clear_member_caches_for_groups([G], 'cont');$out_crel_ids[slug]→ deleteshtax_crel_rev_{slug}_U+htax_reverse_rel_{slug}_U;$out_crel_grp_ids[slug]→ deletes G’s ownhtax_reverse_rel_{slug}_Gthen callshtax_clear_member_caches_for_groups([G], 'crel_rev', slug); (5) clears caches for terms related via T’s group membershipshtax_reload_term_caches_on_edit( int|WP_Term $term_id_or_term, int $tt_id )- Invalidate all caches for a term on edit (no eager reload — lazy-load on next access)
Targeted caches:
htax_clear_count_cache()- Clear total post count caches (htax_total_*)htax_clear_ajax_caches()- Clear autocomplete cacheshtax_delete_transients_by_prefix( string $prefix )- Delete all transients whose key starts with$prefix(both value and timeout rows); used for bulk-clearing type-scoped custom rel caches (htax_crel_{slug}_andhtax_crel_rev_{slug}_), count caches, autocomplete caches, sticky post caches, and similar prefix-keyed groups
Group-aware invalidation:
htax_update_group_membership_caches()- Surgical group member cache updates when term group membership changes. Steps: (1) invalidate+reload term’shtax_groups_*cache; (2) surgically updatehtax_grp_{group_id}_{taxonomy}caches for affected groups + ancestors; (3) invalidate container cache; (4) reload term’s other caches viahtax_reload_term_caches_on_edit(clears all of M’s own transients includinghtax_inc_M,htax_cont_M,htax_crel_*/htax_crel_rev_*_M); (5) schedulewp_schedule_single_event(time(), 'htax_async_group_propagate', [$all_affected_groups])so thathtax_invalidate_group_relationship_cachesruns asynchronously in the next cron pass without blocking the save response. Triggered byhtax_term_groups_changedhook. Receives proper$new_groups/$old_groupsonly when called viahtax_set_term_groups; group-admin.php edit-screen savers callhtax_invalidate_group_relationship_cachesandhtax_clear_member_caches_for_groupsdirectly.htax_invalidate_group_relationship_caches( array $group_ids ): void- Invalidatehtax_inc_*,htax_crel_{slug}_*, andhtax_crel_rev_{slug}_*caches for all terms that include or are container-related to the given groups (and their ancestors/descendants). Runs two SQL queries: (A) inclusions fwd — collect allsource_term_idvalues from T→G rows where G is ingroups_with_ancestors; (B) inclusions rev/container — collect alltarget_term_idvalues from G→T rows where G is ingroups_all(ancestors + descendants). Both collected ID sets are merged and passed tohtax_delete_term_transients_batch()in one call, which batch-deletes named transients and queues async rebuild for all affected terms. Custom rel caches (sections C+D) are still cleared per-term viahtax_delete_transient()since they are slug-keyed and not covered by the batch function. Hooked tohtax_async_group_propagatecron event (fired byhtax_update_group_membership_caches); also called directly byhtax_invalidate_caches_on_group_reparent.htax_clear_member_caches_for_groups( array $group_ids, string $mode, string $cslug = '' ): void- Clear a specific transient for all members of the given groups. Expands groups upward (htax_get_group_ancestors) for'inc'mode, or downward (htax_get_group_hierarchy) for all other modes, then queriesmember_ofrows to find members and deletes the appropriate transient per member. Modes:'inc'deleteshtax_inc_M;'cont'deleteshtax_cont_M;'crel'deleteshtax_crel_{cslug}_M;'crel_rev'deleteshtax_crel_rev_{cslug}_M.htax_invalidate_caches_on_group_reparent( int $term_id, int $tt_id, string $taxonomy ): void- Hooked toedited_termat priority 12. Returns early unless taxonomy is'group'. Reads old parent fromhtax_term_data_{term_id}transient (captured byhtax_cache_term_taxonomy_before_editat priority 5), compares to current parent, and if the parent changed: (1) callshtax_invalidate_group_relationship_caches([old_parent, new_parent])to clearhtax_inc_*,htax_cont_*, andhtax_crel_*/htax_crel_rev_*for terms T that have rows pointing to the old/new parent chains; (2) callshtax_clear_member_caches_for_groups([old_parent, new_parent], 'inc')to clearhtax_inc_Mfor members of the old and new parent groups (and their ancestors), because path 3 ofhtax_get_included_termsexpands member groups downward so ancestor-group members lose or gain G in their expanded set when G moves.
Frontend / shortcode caches:
htax_invalidate_contrib_cache( int $term_id )- Delete allhtax_contrib_{term_id}_p*pages; deduped per request via static arrayhtax_invalidate_post_list_cache( int[] $term_ids )- Pattern-deletehtax_pl_*shortcode caches for given term IDshtax_expand_term_ids_for_contrib_invalidation( int[] $term_ids, string $taxonomy ): int[]- Expand a term ID list to include all terms whose contrib caches are affected: adds WP ancestors, group-expanded included terms, and reverse-included terms (terms that include any of the input terms). Called before firinghtax_term_archive_changedso that the contrib-cache invalidation covers the full upstream fan-out.htax_tt_ids_to_term_ids( int[] $tt_ids ): int[]- Convert term-taxonomy IDs to term IDs via a singleSELECT term_id FROM wp_term_taxonomy WHERE term_taxonomy_id IN (…)query; used indeleted_term_relationshipshook handlers that receive$tt_idsfrom WordPress.htax_get_post_index_term_ids( int $post_id ): int[]- Return all term IDs associated with a post via the denormalizedwp_htax_search_termsindex; used to find which terms’ contrib caches need invalidating when a post’s term assignments change.
Async cron hooks (registered in includes/hooks/actions.php):
htax_async_rebuild_term_caches→htax_do_rebuild_term_caches( array $queue )— Scheduled byhtax_rebuild_queued_term_caches()at shutdown; re-warmshtax_inc_*,htax_cont_*, andhtax_syno_*for each queued term without blocking the current request.htax_async_group_propagate→htax_invalidate_group_relationship_caches( array $group_ids )— Scheduled byhtax_update_group_membership_caches()after a group membership change; propagates cache invalidation to all terms that include or are related to the affected groups, asynchronously.
Utilities & Helpers (includes/core/helpers.php)
Shared internal utilities used throughout the plugin. Not part of the public API.
SQL Utilities:
htax_sql_placeholders()- Generate SQL placeholders for prepared statements
Term ID Utilities:
htax_sanitize_term_ids()- Sanitize and validate array of term IDshtax_parse_term_ids($meta_value, $taxonomy = '')- Parse a comma-separated term list into term IDs; numeric tokens are IDs, and when$taxonomyis given, non-numeric tokens are resolved as slugs within it (slugs are unique per taxonomy). Without a taxonomy, only numeric IDs are kept. Slugs resolve to IDs before any cache key is built. Filters out zero/negative values
Term Validation:
htax_get_valid_term(int $term_id)- Safely get and validate a term; no taxonomy parameter (WP term IDs are globally unique); per-request cache (htax_request_cache( 'valid_term' )) capped at 500 entries (FIFO eviction) to prevent unbounded memory growth in long-running requests; entries are dropped on WordPress’sclean_term_cacheaction, so a term updated or deleted earlier in the request is never returned stalehtax_request_cache( string $group ): array&/htax_clear_request_cache( string $group, $key = null )- Named per-request caches. Used instead of function-level statics wherever the cached data can change later in the same request, so the code that invalidates the data (or its transient) can clear the per-request copy too. Groups:redirect_target,redirect_term_ids,synonyms,valid_term,taxonomy_enabled,indexed_post_types,enabled_taxonomies.htax_clear_settings_request_caches()- Hooked first (priority 0) onupdate_option_htax_settings; clears the per-request copies behindhtax_is_taxonomy_enabled(),htax_get_indexed_post_types()andhtax_get_enabled_taxonomies(), so handlers later in a settings-save request read the new settings
Admin Notices:
htax_add_admin_notice()- Add admin notice (immediate or queued)htax_display_queued_notices()- Display notices queued from previous requests
Security:
htax_verify_term_nonce()- Verify WordPress term edit/add nonces; accepts three nonce sources:_wpnonce(actionupdate-tag_{id}, full edit form),_wpnonce_add-tag(actionadd-tag, AJAX new-term form), and_inline_edit(actiontaxinlineeditnonce, WP Quick Edit). Thehtax_save_term_dataearly-return guard checks for the presence of any of these three fields before calling this function.
Logging & Error Handling:
htax_log_debug()- Debug logging (WP_DEBUG + WP_DEBUG_LOG, supports verbose backtrace mode)htax_log_error()- Error logging (always logs, supports verbose backtrace mode)htax_get_filtered_backtrace()- Generate filtered backtrace string (excludes logging frames)htax_handle_db_error()- Check wpdb->last_error; returns$default(and logs) on error, returnsfalsewhen no error. Callers useif ( false !== $err = htax_handle_db_error(...) ) { return $err; }to short-circuit on failure.
Meta Key Helpers:
htax_get_sticky_assignment_key(int $term_id)- Get the post meta key for a sticky term assignment (_htax_sticky_{term_id}); taxonomy is omitted because WordPress term IDs are globally unique
Search Query Building:
htax_parse_search_query( string $query, string $near_country = 'US' ): array- Parse a free-text search string into structured per-field arrays- Returns keys:
title,body,excerpt,tags,categories,terms,general,proximity - Each field value is an array of
['term', 'required', 'excluded', 'quoted', 'slug_match']entries +termsetsrequired=true;-termsetsexcluded=true;"term"setsquoted=trueslug_match=truefor singular operators (tag:,category:,term:, taxonomy slug, singular display label) — callers match against term slugs rather than FULLTEXT names.slug_match=falsefor plural operators (tags:,categories:,terms:, plural display label) — FULLTEXT matching.quoted=truefor field-specific terms if the value was wrapped in"..."in the input; for general terms,quoted=trueonly when the term appeared as"word"— used by exact-match scoring to distinguish intentional label lookups from content searchesproximitykey: array of parsednear:tokens (see ATCF Integration). Empty when ATCF is inactive or no proximity operators are present.$near_country: ISO 3166-1 code passed down to proximity tokens for postal-code format validation and centroid lookup. Defaults to'US'.- Used by all three search systems: post list, advanced widget, PowerSearch
- Returns keys:
htax_get_exact_match_term_ids( string $term_string, string $taxonomy = '' ): array— B-tree equality lookup: returns term IDs whereLOWER(name) = LOWER($term_string), optionally filtered to one taxonomy. Used by PowerSearch to pre-fetch IDs for exact-match scoring.htax_build_boolean_query_string( array $field_terms, array $general_terms = [] ): string- Build a MySQL FULLTEXT BOOLEAN MODE query string (used for taxonomy scoring and WHERE enforcement)htax_build_natural_language_query_string( array $field_terms, array $general_terms = [] ): string- Build a NATURAL LANGUAGE MODE query string (operators stripped, excluded terms omitted; used for post text field scoring)- Accepts a flat array of
['term', 'required', 'excluded']entries - Multi-word phrases are wrapped in double quotes;
+/-prefixes applied without modifying term content - Replaces the removed
htax_build_boolean_query()(which had a bug wherestr_replacestripped-from term content)
- Accepts a flat array of
htax_build_term_filter_clauses( array $filter_term_ids, array $filter_group_term_sets, array $filter_out_term_ids ): array- Build SQL AND clauses for post-by-term filtering- All clauses use
pt.post_id IN (SELECT post_id FROM wp_htax_search_terms WHERE ...)subqueries; assumes query useswp_htax_post_text AS pt $filter_term_ids: AND across all IDs — each gets its own subquery$filter_group_term_sets: array of arrays — AND across groups, OR within each group$filter_out_term_ids: NOT IN exclusion — single subquery- Returns empty array when all inputs are empty
- All clauses use
htax_get_hidden_term_filter_clause()- Get SQL WHERE clause for filtering hidden terms- Returns empty string if user can see hidden terms
- Returns
AND is_hidden = 0clause otherwise - Ensures consistent permission-based filtering across all queries
Search Response Formatting:
htax_format_search_result_post()- Format post object for search result display- Standardized formatting for AJAX responses across PowerSearch and widget searches
- Returns array with: id, title, excerpt, permalink, thumbnail, date, modified, author, comment_count
- Accepts optional image_size parameter (default: ‘thumbnail’)
htax_build_pagination_response()- Build pagination response structure- Creates consistent response arrays with pagination metadata
- Returns array with: posts, found_posts, results_label (translated, pluralized “N results found”), max_pages, current_page, notices
- Used across all search AJAX endpoints
htax_process_post_placeholders( string $text, WP_Post $post ): string- Replace{placeholder}tokens in a template string with post data- Tokens:
{id},{title},{url},{date},{modified},{author},{excerpt},{excerpt_text},{comment_count},{post_type},{slug},{image_src},{image_alt},{image_width},{image_height},{thumbnail},{thumbnail:size} {thumbnail}/{thumbnail:size}— full<img>HTML fromget_the_post_thumbnail()at the specified WP registered size (defaultmedium); multiple distinct sizes in the same template each resolve independently- Expensive tokens (author meta, excerpt, featured image, thumbnail) are lazy-evaluated via
strpos/preg_matchcheck - Output is passed through
wp_kses_post() - Used by
[ht_post_list],[ht_related_posts], and[ht_search_results]forprepend/appendattributes and themetatemplate attribute
- Tokens:
Hook Organization
Action Hooks (includes/hooks/actions.php)
Organized by category:
- Initialization and Setup (6 hooks —
plugins_loaded×3,admin_init×2,init×1) - Admin Enqueue Scripts (2 hooks)
- Admin Notices (1 hook)
- Term Edit Form Fields (dynamic on
init; per non-group taxonomy: up to 14 hooks — field handlers at priorities 8–18 × add/edit variants plus collapsible open/close at priorities 9+19; group taxonomy gets only the field handlers that have their own group guards). The External Links field is registered by TermConnect (when active); collapsible hooks are not registered for group at all. - Term Save Hooks (3 hooks per enabled taxonomy —
create_{tax}p5,edited_{tax}p20/p30, dynamic oninit) - Cache Management (14 hooks — includes surgical hidden-terms meta hooks and post-list cache invalidation)
- Term Edit Pre-Cache (1 hook —
edit_termp5 to cache hierarchy data for search-index parent-change detection) 7a. External Conversion Detection (3 hooks —created_termp1,pre_delete_termp5,delete_termp5) - Cleanup Hooks (5 hooks —
edited_term,created_term,delete_term,delete_term_taxonomy,delete_post) - Query Modification (7 hooks — 6×
pre_get_posts+template_redirect)- Note:
htax_apply_archive_filter(pre_get_postspriority 11) is registered directly fromclass-filter-options.php, not from hooks/actions.php - Note:
htax_expand_secondary_query(pre_get_postspriority 10) is registered directly fromintegration-api.php;htax_fire_archive_changed_on_hierarchy(edited_termpriority 11) and thehtax_redirect_changedclosure are registered fromactions.phpbut their callbacks live incache.php
- Note:
- Frontend Display (4 hooks —
wp_head×3,admin_head×1) - AJAX Handlers (4 hooks)
- Admin UI Enhancements — hooks in
includes/admin/tables.php(enqueued viahtax_enqueue_table_assets) - Security (1 hook —
send_headers) - Multisite Support (1 hook —
wp_initialize_site) - Custom Relationship Hooks (4 hooks —
init×2 for type registration/loading,htax_relationship_savedandhtax_relationship_deletedfor search index requeue)
Filter Hooks (includes/hooks/filters.php)
Organized by category:
- Plugin Meta and Links (2 filters)
- Term Visibility (3 filters)
- Admin Columns (dynamic per-taxonomy: redirect, icon, hidden, bulk action filters)
- Query Filtering (4 admin filters — redirect, icon, visibility, group)
- Archive Display (1 filter —
get_the_archive_title) - Post Styling (1 filter)
- Meta Box Defaults (1 filter)
- The_Posts Filter (1 filter)
- Pagination (1 filter)
- Admin Term Links (dynamic per-taxonomy sticky indicators)
- Admin Styling (1 filter)
- Tag Creation Gate (1 filter —
pre_insert_term→htax_maybe_prevent_tag_creation; function defined incore/functions.php)
Integration API Hooks (registered directly from integration-api.php)
htax_expanded_term_ids— filter on expanded term ID list (registered withinhtax_get_expanded_term_ids)htax_term_post_count— filter on per-term expansion-aware count (registered fromhtax_adjust_term_counts)get_termsfilter (htax_adjust_term_counts) — active on frontend only whenexpand_secondary_queriesis onrest_api_init— registers REST filters that enrich term responses withhtax_*fields, enforce hidden-term visibility, and normalize HTML entity encoding in term name searches (registered fromincludes/hooks/filters.php). The entity normalization filter hooksget_terms_argsfor the lifetime of the REST request: WP stores term names HTML-encoded inwp_terms.name(e.g.H&M), but REST clients (Gutenberg tag autocomplete) send raw text; without normalization, searchingh&mnever matchesH&M. The filter applieshtml_entity_decode→htmlspecialcharstoargs['search']using the same round-trip ashtax_run_term_autocomplete_search().wp_sitemaps_taxonomies_query_args— excludes hidden term archives from XML sitemaps (registered fromincludes/hooks/filters.php)
Hook Execution Order
Event: edited_term
| Priority | Handler | Action |
|---|---|---|
| 10 | htax_reload_term_caches_on_edit | Reload term caches (htax_inc_*, htax_cont_*, htax_excl_*, htax_redir_*, htax_groups_*, htax_direct_rel_*, htax_direct_rel_grp_*, htax_crel_*, htax_crel_rev_* for all registered slugs) |
| 15 | htax_clear_ajax_caches | Clear autocomplete caches |
| 20 | htax_cleanup_hierarchy_conflicts | Remove conflicting relationships, fire reindex actions |
edited_{$taxonomy} also fires (separate WordPress hook):
- Priority 20:
htax_save_term_data— save relationships (includes/containers, redirects, custom type tab rows), eager reload caches, fire custom actions - Priority 30:
htax_clear_count_cache_on_save— targeted count cache clear
Critical timing: Cache is reloaded INSIDE relationship setters (htax_save_term_relationships(), htax_set_redirect(), htax_remove_redirect()) AFTER the DB update but BEFORE custom actions fire. This ensures index handlers always read the new state from cache, preventing race conditions.
Event: edited_term (hierarchy handler)
In addition to the edited_term handlers listed above, a dedicated hierarchy handler runs for all hierarchical taxonomies (except group, which has its own handler):
| Priority | Handler | Action |
|---|---|---|
| 10 | on_taxonomy_hierarchy_changed | Batched DELETE + INSERT of hierarchy entries when parent changes |
Hierarchy change optimization: identifies changed term plus all descendants, deletes old hierarchy entries in one query, inserts all new entries in one batched INSERT (ON DUPLICATE KEY UPDATE). Handles grandchildren, great-grandchildren, etc. Skips non-hierarchical taxonomies and the group taxonomy (handled by on_group_hierarchy_changed via edited_group).
Event: created_term
| Priority | Handler | Action |
|---|---|---|
| 1 | htax_track_created_term_for_conversion | Records the new term in $GLOBALS['htax_recently_created_terms'] (keyed by lowercase name) for external-conversion detection — runs before all other HyperTaxonomy hooks |
| 20 | htax_cleanup_hierarchy_conflicts | Remove conflicting relationships, fire reindex actions |
| 21 | htax_cascade_hidden_on_create | Inherit hidden status from parent |
Event: pre_delete_term
| Priority | Handler | Action |
|---|---|---|
| 5 | htax_detect_external_conversion_pre_delete | Checks whether the term being deleted has the same name as a recently created term in a different taxonomy (external-conversion pattern). If matched: captures groups, hierarchy, icon ID, hidden status, and sticky post assignments into $GLOBALS['htax_pending_external_conversions'] before term meta is wiped by wp_delete_term(). Sticky assignments are captured here (not at delete_term) because delete_term_taxonomy fires before delete_term and htax_cleanup_deleted_term deletes sticky postmeta via delete_term_taxonomy at priority 10. |
Event: delete_term
| Priority | Handler | Action |
|---|---|---|
| 5 | on_term_deleted (search index) | DELETE FROM wp_htax_search_terms WHERE source_term_id = %d |
| 5 | htax_complete_external_conversion_after_delete | If $GLOBALS['htax_pending_external_conversions'] has an entry for this term (and htax_native_conversion_in_progress is not set): transfers hidden status, icon (if dimensions match), and sticky posts to the new term; queues reindex; fires htax_term_converted. Group memberships are not transferred — no stub means no taxonomy-correct recipient. No redirect stub is created. Sticky assignments were captured at pre_delete_term time (before delete_term_taxonomy cleans them up) and are replayed here directly. |
| 10 | htax_invalidate_term_caches_on_delete | Invalidate term caches |
| 10 | htax_cleanup_deleted_term | Delete from wp_htax_term_relationships (all relationship types) + term meta + _htax_sticky_{term_id} post meta |
| 15 | htax_clear_ajax_caches | Clear autocomplete caches |
The two priority-10 handlers are independent (different tables) and have no ordering dependency.
Event: set_object_terms
| Priority | Handler | Action |
|---|---|---|
| 10 | on_term_relationships_added | Delta-based incremental index update — diffs the indexed source terms against the post’s actual terms read from the database (the hook’s $terms holds only the appended terms on wp_add_object_terms(); deleted_term_relationships → on_term_relationships_removed() reuses the same path); targeted count cache clear |
Incremental update: Queries current source terms from index (1 query), calculates delta, deletes removed term relationships by source_term_id (1 query), inserts new ones (~4–9 queries per new term). Approximately 5× faster than full rebuild for typical 1–3 term changes. The inserted rows carry the column-default strength, so when terms were added the post is also queued (queue_post_reindex()) for a background reindex that computes their strengths.
Event: delete_post / trash_post
| Priority | Handler | Action |
|---|---|---|
| 10 | htax_cleanup_sticky_on_delete | Delete sticky post meta (delete_post only) |
| 10 | on_post_delete (search index) | Delete from search index (both hooks) |
Event: save_post
| Priority | Handler | Action |
|---|---|---|
| 10 | htax_sync_post_text_row | Upsert row in wp_htax_post_text for published, draft, pending, future, and private posts using htax_extract_post_body(); delete row for trash and auto-draft. Skips revisions and autosaves. Draft rows enable content-based tag suggestions in the editor. Force-deleted posts that bypass trash may leave an orphan row until the next full rebuild. |
| 20 | on_post_saved (search index) | Queue a published, indexed-type post for reindex so its stored strengths reflect the post text just refreshed at priority 10. Skips revisions and autosaves. |
Event: wp_update_comment_count
| Priority | Handler | Action |
|---|---|---|
| 10 | htax_sync_post_text_comment | Update comment_count and last_comment_gmt in wp_htax_post_text. No-ops if the post row does not exist in the table. |
Event: transition_post_status
| Priority | Handler | Action |
|---|---|---|
| 10 | on_post_status_change | Remove from index if unpublished; queue reindex if newly published |
Skips if both old and new status are the same publish-state. When a scheduled post auto-publishes via cron, the transition fires and triggers a fresh index build.
Custom Actions (Relationship Changes)
Fired by htax_save_term_relationships() and htax_set_redirect() / htax_remove_redirect() when relationships change:
| Action | Parameters | Index Strategy |
|---|---|---|
htax_includes_changed | $term_id, $taxonomy, $new_included, $old_included | Direct SQL: update container rows in posts with newly/no-longer-included terms |
htax_containers_changed | $term_id, $taxonomy, $new_containers, $old_containers | Direct SQL delta: DELETE removed + INSERT added container rows |
htax_excludes_changed | $term_id, $taxonomy, $new_excluded, $old_excluded | Queue reindex of posts with included group members (exclusion logic too complex for SQL) |
htax_included_groups_changed | $term_id, $taxonomy, $new_groups, $old_groups | Queue reindex of posts with members of affected groups |
htax_custom_inclusions_changed | $term_id, $taxonomy, $affected_term_ids | on_custom_inclusions_changed(): queue reindex of posts with the affected (included-side) terms |
htax_container_groups_changed | $term_id, $taxonomy, $new_groups, $old_groups | Direct SQL delta via unified container handler (groups expanded by cache) |
htax_redirect_changed | $term_id, $taxonomy, $new_target_id, $new_taxonomy, $old_target | DELETE old redirect_target rows + INSERT new ones; on first redirect also DELETE container entries for the term and, for hierarchical taxonomies, DELETE hierarchy entries where term is ancestor or where descendants point to ancestors-via-this-term. $old_target is null on first redirect, WP_Term on target change. htax_term_archive_changed fires only when $old_target is null — changing an existing redirect’s target has no archive effect |
htax_redirect_removed | $term_id, $taxonomy, $old_target_id, $old_taxonomy | DELETE redirect_target rows for this source term; restore container entries from preserved inclusions; for hierarchical taxonomies, restore hierarchy entries via on_hierarchy_parent_restored() |
htax_term_groups_changed | $term_id, $taxonomy, $new_groups, $old_groups | Queue reindex of posts with the term + containers + terms with this group included |
htax_term_visibility_changed | $term_id, $taxonomy | Direct SQL UPDATE of is_hidden flag for all rows where changed term appears as source_term_id or term_id; no reindex needed |
htax_relationship_saved | $source_term_id, $target_term_id, $type_slug | Fired by htax_add_term_relationship() after a relationship row is inserted; queues reindex for both source and target terms |
htax_relationship_deleted | $source_term_id, $target_term_id, $type_slug | Fired by htax_remove_term_relationship() after a relationship row is deleted; queues reindex for both source and target terms |
Dual handler architecture: Each action has two handlers at priority 10 — one for index updates, one for cache invalidation. Both process old and new relationship values for complete coverage.
Type registration action: htax_relationship_types fires at the end of htax_register_builtin_relationship_types() (init priority 0), after all four built-in types are registered. Plugin code that calls htax_register_relationship_type() in PHP should hook here rather than using init directly, to guarantee built-in types are already registered when the hook fires.
In addition to the per-operation actions above, htax_term_archive_changed( $term_id, $taxonomy ) is the unified signal for any change to a term’s archive composition. It fires for: every relationship save/delete; a term’s first redirect (not target changes); redirect removal; term parent changes (via htax_fire_archive_changed_on_hierarchy in cache.php, at edited_term priority 11, firing for old/new parent ancestor chains); post-term assignment changes (fires for directly-assigned terms and their containers/ancestors via htax_expand_term_ids_for_contrib_invalidation); post publish/unpublish; and sticky post changes. Internally, HyperTaxonomy hooks it at priority 10 to invalidate the htax_contrib_ paginated archive cache (htax_invalidate_contrib_cache). The named callback htax_fire_term_archive_changed (also in cache.php) is used by add_action registrations in actions.php to bridge fine-grained relationship actions to this signal. External integrations can hook it to invalidate their own derived data without subscribing to each fine-grained action.