= AI_BASE (id = AI_BASE + term_id), in the database * as target_term_id with target_post_id = 0 and the archive URL in custom_url. */ class Linkbraid_Terms { const AI_BASE = 1000000000; const URL_CACHE = 'lbraid_term_urls2'; /** * Taxonomies whose archives may RECEIVE links. Option lbraid_taxonomies; * never saved => product categories only (post-category archives are thin * lists on most sites and make poor link targets — enabling them is a * conscious editorial choice), saved empty => the feature is off. */ public static function taxonomies() { $saved = get_option('lbraid_taxonomies', null); if ($saved === null || $saved === false) { return taxonomy_exists('product_cat') ? ['product_cat'] : []; } if ($saved === '') return []; return array_values(array_filter(array_map('sanitize_key', explode(',', (string) $saved)), 'taxonomy_exists')); } /** * Taxonomies the SCANNER recognizes: every public taxonomy of the enabled * post types. Links to any archive are counted and deduplicated even when * that archive is not allowed as an AI target. */ public static function scan_taxonomies() { $out = array_keys(self::choices()); foreach (self::taxonomies() as $taxonomy) if (!in_array($taxonomy, $out, true)) $out[] = $taxonomy; return $out; } /** Choices for the settings screen: public taxonomies of the selected post types. */ public static function choices() { $out = []; foreach (get_taxonomies(['public' => true], 'objects') as $slug => $taxonomy) { if (in_array($slug, ['post_format'], true)) continue; if (!array_intersect((array) $taxonomy->object_type, lbraid_post_types())) continue; $out[$slug] = $taxonomy->labels->name; } return $out; } /** Normalized same-host URL key (path + query, no scheme/fragment), '' for foreign hosts. */ public static function normalize($url) { $parts = wp_parse_url((string) $url); if (!$parts) return ''; $home_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); $host = strtolower((string) ($parts['host'] ?? $home_host)); if (preg_replace('/^www\./', '', $host) !== preg_replace('/^www\./', '', $home_host)) return ''; $path = untrailingslashit(strtolower((string) ($parts['path'] ?? '/'))); $query = isset($parts['query']) ? '?' . $parts['query'] : ''; return ($path === '' ? '/' : $path) . $query; } /** Map of normalized archive URL => ['term_id' => ..., 'taxonomy' => ...]. Cached 12 h. */ public static function url_map() { static $map = null; if ($map !== null) return $map; $cached = get_transient(self::URL_CACHE); if (is_array($cached)) return $map = $cached; $map = []; foreach (self::scan_taxonomies() as $taxonomy) { $terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 2000]); if (is_wp_error($terms)) continue; foreach ($terms as $term) { $link = get_term_link($term); if (is_wp_error($link)) continue; $key = self::normalize($link); if ($key !== '') $map[$key] = ['term_id' => (int) $term->term_id, 'taxonomy' => $taxonomy]; } } set_transient(self::URL_CACHE, $map, 12 * HOUR_IN_SECONDS); return $map; } /** Term the internal URL points to, or null. */ public static function resolve($url) { $key = self::normalize($url); if ($key === '') return null; $map = self::url_map(); return $map[$key] ?? null; } /** * Validated term with its archive URL, or null. By default the term must be * an allowed TARGET; $for_target = false accepts anything the scanner knows * (Braid Map nodes, reports on existing links). */ public static function get($term_id, $for_target = true) { $term = get_term((int) $term_id); $allowed = $for_target ? self::taxonomies() : self::scan_taxonomies(); if (!$term || is_wp_error($term) || !in_array($term->taxonomy, $allowed, true)) return null; $link = get_term_link($term); if (is_wp_error($link)) return null; return ['term_id' => (int) $term->term_id, 'taxonomy' => $term->taxonomy, 'name' => $term->name, 'url' => $link, 'count' => (int) $term->count]; } /** AI candidates: the busiest archives first, already linked ones excluded. */ public static function candidates($limit = 20, $exclude_term_ids = []) { $out = []; foreach (self::taxonomies() as $taxonomy) { $terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => true, 'number' => $limit, 'orderby' => 'count', 'order' => 'DESC']); if (is_wp_error($terms)) continue; foreach ($terms as $term) { if (in_array((int) $term->term_id, $exclude_term_ids, true)) continue; $link = get_term_link($term); if (is_wp_error($link)) continue; $out[self::AI_BASE + (int) $term->term_id] = [ 'id' => self::AI_BASE + (int) $term->term_id, 'title' => $term->name, 'type' => 'archive', 'priority' => false, 'weight' => 4, 'preferred_anchors' => [], 'anchor_mode' => 'prefer_exact', 'anchor_hash' => '', ]; } } return array_slice($out, 0, $limit, true); } }