Jake Vanderwerf
2026-05-01 48721c85ebcfa973ee81719d2467ca80e4253dc9
inc/rest/routes/FeedRoutes.php
@@ -1,13 +1,12 @@
<?php
namespace JVBase\rest\routes;
use JVBase\managers\CacheManager;
use JVBase\rest\RestRouteManager;
use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\integrations\Umami;
use JVBase\meta\MetaManager;
use JVBase\managers\TaxonomyRelationships;
use JVBase\utility\Checker;
use JVBase\utility\Features;
use JVBase\rest\Route;
use JVBase\base\Site;
use WP_Query;
use WP_Post;
use WP_Term;
@@ -18,53 +17,36 @@
    exit; // Exit if accessed directly
}
class FeedRoutes extends RestRouteManager
class FeedRoutes extends Rest
{
   protected int $per_page = 36;
   protected ?Umami $tracker = null;
   protected ?Checker $checker = null;
   protected ?array $fields = null;
   protected ?array $timelineSharedFields = null;
   protected ?array $timelineUniqueFields = null;
   public function __construct()
   {
      $this->cache_name = 'feed';
      $this->cache_ttl = 86400;
      $this->cacheName = 'feed';
      $this->cacheTtl = 86400;
      parent::__construct();
      $this->cache
         ->connect('post', true)
         ->connect('taxonomy', true)
         ->connect('user', true);
      if (JVB_TESTING) {
         $this->cache->flush();
      }
   }
   public function init():void
   {
      $this->checker = Checker::getInstance();
      if (jvbSiteUsesUmami()) {
      if (Site::hasIntegration('umami')) {
         $this->tracker = JVB()->connect('umami');
      }
      $this->cache->clear();
      $this->setupCacheConnections();
   }
   /**
    * Set up cache connections for automatic invalidation
    */
   protected function setupCacheConnections(): void
   {
      // Connect to all content types with show_feed
      $contentTypes = Features::getTypesWithFeature('show_feed', 'content');
      foreach ($contentTypes as $type) {
         CacheManager::for('feed_item_'.$type)->connectTo('post');
         $this->cache->connectTo('post', $type);
      }
      // Connect to all taxonomies with show_feed
      $taxonomies = Features::getTypesWithFeature('show_feed', 'taxonomy');
      foreach ($taxonomies as $tax) {
         CacheManager::for('feed_item_'.$tax)->connectTo('taxonomy');
         $this->cache->connectTo('taxonomy', $tax);
      }
   }
   /**
@@ -73,17 +55,53 @@
    */
   public function registerRoutes(): void
   {
      register_rest_route($this->namespace, '/feed', [
         'methods' => ['GET', 'POST'],
         'callback' => [$this, 'handleFeedRequest'],
         'permission_callback' => [$this, 'checkPermission'],
      ]);
      Route::for('feed')
         ->get([$this, 'handleFeedRequest'])
         ->args([
            'content' => 'string',
            'page' => 'integer|default:1|min:1',
            'taxonomy' => 'string',
            'match' => 'string|enum:all,any|default:all',
            'orderby' => 'string',
            'order' => 'string|enum:ASC,DESC',
            'date-filter' => 'string',
            'dateFrom' => 'string',
            'dateTo' => 'string',
            'context' => 'string',
            'source' => 'string',
            'favourites' => 'boolean',
            'user' => 'integer',
            'highlight' => 'string',
         ])
         ->auth('public')
         ->rateLimit(30, 60)
         ->post([$this, 'handleFeedRequest'])
         ->args([
            'content' => 'string',
            'page' => 'integer|default:1|min:1',
            'taxonomy' => 'string',
            'match' => 'string|enum:all,any|default:all',
            'orderby' => 'string',
            'order' => 'string|enum:ASC,DESC',
            'date-filter' => 'string',
            'dateFrom' => 'string',
            'dateTo' => 'string',
            'context' => 'string',
            'source' => 'string',
            'favourites' => 'boolean',
            'user' => 'integer',
            'highlight' => 'string',
         ])
         ->auth('public')
         ->rateLimit(30)
         ->register();
      register_rest_route($this->namespace, 'feed/types', [
         'permission_callback' => [$this, 'checkPermission'],
         'methods' => 'GET',
         'callback' => [$this, 'getFeedTypes']
      ]);
      // Feed types endpoint
      Route::for('feed/types')
         ->get([$this, 'getFeedTypes'])
         ->auth('public')
         ->rateLimit()
         ->register();
   }
   /**
@@ -99,46 +117,51 @@
            $post = get_post($postID);
            $type = jvbNoBase($post->post_type);
            $metaType = 'post';
            $cache = CacheManager::for('feed_item_'.$type);
            break;
         default:
            $post = get_term($postID, jvbCheckBase($type));
            $type = jvbNoBase($type);
            $metaType = 'term';
            $cache = CacheManager::for('feed_item_'.$type);
            break;
      }
      if (!$post || is_wp_error($post)) {
         return [];
      }
      return $cache->remember($postID,
      return $this->cache->remember(
         $postID,
         function() use ($postID, $type, $metaType, $post, $skip) {
            $config = null;
            $registrar = null;
            switch ($metaType) {
               case 'post':
                  $config = JVB_CONTENT[$type];
                  if (!$skip && array_key_exists('is_timeline', $config) && $config['is_timeline']) {
                  $registrar = Registrar::getInstance($type);
                  $meta = Meta::forPost($postID);
                  if (!$skip && $registrar->isTimeline()) {
                     return $this->formatTimeline($postID, $post);
                  }
                  break;
               case 'term':
                  $config = JVB_TAXONOMY[$type];
                  $meta = Meta::forTerm($postID);
                  $registrar = Registrar::getInstance($type);
                  break;
               case 'user':
                  $meta = Meta::forUser($postID);
                  $registrar = Registrar::getInstance($type);
                  break;
            }
            if (!$config) {
            if (!$registrar) {
               return [];
            }
            $fields = $config['fields'];
            $fields = $registrar->getFields();
            //Allow custom filtering for public fields
            if (array_key_exists('feed', $config) && array_key_exists('fields', $config['feed'])) {
               $fields = array_filter($fields, function($field) use ($config) {
                  return in_array($field, $config['feed']['fields']);
            if (!empty($registrar->getConfig('feed')['fields'])) {
               $fields = array_filter($fields, function($field) use ($registrar) {
                  return in_array($field, $registrar->getConfig('feed')['fields']);
               }, ARRAY_FILTER_USE_KEY);
            }
            $meta = new MetaManager($postID, $metaType);
            $values = $meta->getAll(array_keys($fields));
            $out = [
@@ -146,42 +169,17 @@
            ];
            //Format Taxonomies
            $temp = array_filter($fields, function($field) {
               return $field['type'] === 'taxonomy';
            });
            foreach ($temp as $key => $config) {
               if (array_key_exists($key, $out) && $out[$key] !== '') {
                  $IDs = array_map('absint', explode(',', $out[$key]));
                  $data = [];
                  $icon = JVB_TAXONOMY[$config['taxonomy']]['icon']??'triangle';
                  foreach ($IDs as $ID) {
                     $term = get_term($ID, jvbCheckBase($config['taxonomy']));
                     if ($term && !is_wp_error($term)) {
                        $data[$ID] = [
                           'id'  => $ID,
                           'icon'   => $icon,
                           'name'   => $term->name,
                           'url' => get_term_link($ID, jvbCheckBase($config['taxonomy'])),
                        ];
                        if ($this->tracker) {
                           $data[$ID]['umami_click'] = $this->tracker->trackClick($ID, $config['taxonomy'], ['from' => $type.'_'.$postID]);
                        }
                     }
                  }
                  if (!empty($data)) {
                     $out['fields'][$key] = $data;
                  }
               }
            }
            $out['taxonomies'] = $this->extractTaxonomies($values, $postID, $type);
            //Add images
            $imgIDs = [];
            $temp = array_filter($fields, function($field) {
               return in_array($field['type'], [ 'upload', 'image', 'gallery']);
            });
            foreach ($temp as $key => $config) {
               if (array_key_exists($key, $out) && $out[$key] !== '') {
                  $IDs = array_map('absint', explode(',',$out[$key]));
               if (array_key_exists($key, $out['fields']) && $out['fields'][$key] !== '') {
                  $IDs = array_map('absint', explode(',',$out['fields'][$key]));
                  foreach ($IDs as $ID) {
                     $imgIDs[$ID] = jvbImageData($ID);
                  }
@@ -191,7 +189,10 @@
            $out['id'] = $postID;
            $out['content'] = $type;
            $out['icon'] = $config['icon']??'triangle';
            $out['icon'] = $registrar->getIcon()??jvbDefaultIcon();
            if ($out['icon'] === '') {
               $out['icon'] = jvbDefaultIcon();
            }
            if ($this->tracker) {
               $args = ($metaType === 'post') ? ['owner_id' => $post->post_author] : [];
@@ -202,32 +203,36 @@
            switch ($metaType) {
               case 'term':
                  $owner = (in_array($type, jvbContentTaxonomies()) ? $meta->getValue('owner') : null);
                  $owner = $registrar->hasFeature('is_content') ? $meta->get('owner') : null;
                  if (!is_null($owner)) {
                     $out['user_id'] = $owner;
                  }
                  $out['url'] = get_term_link($postID, $type);
                  $out['title'] = html_entity_decode($post->name);
                  break;
               case 'post':
                  $out['date'] = $post->post_date;
                  $out['modified'] = $post->post_modified;
                  $out['user_id'] = (int)$post->post_author;
                  $out['url'] = get_the_permalink($postID);
                  $out['title']= get_the_title($postID);
                  break;
            }
            return $out;
         }
      );
   }
   protected function initTimelineFields(string $content):void
   {
      $content = jvbNoBase($content);
      if (!Features::forContent($content)->has('is_timeline')){
      $registrar = Registrar::getInstance($content);
      if (!$registrar || !$registrar->hasFeature('is_timeline')){
         return;
      }
      $config = Features::getConfig($content);
      $this->fields = $config['fields'];
      $this->fields = $registrar->getFields();
      $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
         if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
@@ -253,80 +258,139 @@
      }
      $item = $this->formatItem($postID, 'post', true);
      //Step 1: Get the fields that apply to all posts
      $mainMeta = new MetaManager($post->ID, 'post');
      $mainMeta = Meta::forPost($post->ID);
      $item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
      //Step 2: Get the fields for each individual posts
      $children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish'], 'fields'=> 'ids']);
      array_unshift($children, $post->ID);
      $item['taxonomies'] = $this->extractTaxonomies($item['fields'], $postID, jvbNoBase($post->post_type));
      $subFields = [];
      $images = [];
      foreach ($children as $child) {
         $meta = new MetaManager($child, 'post');
         $meta = Meta::forPost($child);
         $f = $meta->getAll($this->timelineUniqueFields);
         $f =  ['id' => $child] + $f;
         $subFields[] = $f;
         $item['taxonomies'] = array_merge($item['taxonomies'], $this->extractTaxonomies($f, $postID, jvbNoBase($post->post_type)));
         $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
      }
      $item['fields']['order'] = $subFields;
      $item['number'] = (int)get_post_meta($post->ID,BASE.'number', true);
      $item['fields']['before'] = get_post_thumbnail_id($children[0]);
      $item['fields']['after'] = get_post_thumbnail_id($children[array_key_last($children)]);
      $item['fields']['timeline'] = $subFields;
      $item['images'] = $item['images'] + $images;
      return $item;
   }
   protected function extractTaxonomies(array $fields, int $postID, string $content):array {
      $taxonomies = [];
      foreach ($fields as $key => $value) {
         if (empty($value)) {
            continue;
         }
   protected function formatTaxonomy(WP_Term $term, int $postID, string $type)
         $registrar = Registrar::getInstance($key);
         if (!$registrar || $registrar->registrar->public === false){
            continue;
         }
         $terms = array_map('absint', explode(',', $value));
         $terms = array_filter($terms); // Remove 0 values
         if (empty($terms)) {
            continue;
         }
         foreach($terms as $termID) {
            $term = get_term($termID, jvbCheckBase($key));
            if ($term && !is_wp_error($term)) {
               $taxonomies[$key][$termID] = $this->formatTaxonomy($term, $postID, $content);
            }
         }
      }
      return $taxonomies;
   }
   protected function formatTaxonomy(WP_Term|int $term, int $postID, string $type)
   {
      return [
         'ID' => $term->term_id,
         'title' => htmlspecialchars_decode($term->name),
         'url' => get_term_link($term->term_id, $term->taxonomy),
         'umami_click' => $this->tracker->trackTaxonomyClick($term->term_id, $term->taxonomy, [
            'from' => $type . '_' . $postID
         ])
      ];
      return $this->cache->remember(
         $term->term_id,
         function () use ($term, $postID, $type) {
            $base = [
               'ID' => $term->term_id,
               'title' => html_entity_decode($term->name),
               'url' => get_term_link($term->term_id, $term->taxonomy),
            ];
            if ($this->tracker) {
               $base['umami_click'] =$this->tracker->trackTaxonomyClick($term->term_id, $term->taxonomy, [
                  'from' => $type . '_' . $postID
               ]);
            }
            return $base;
         }
      );
   }
   protected function getAuthorData(WP_Post $post)
   {
      $author = $this->cache->get($post->post_author, 'author_data');
      if (!$author) {
         $author = [
            'id' => $post->post_author,
            'label' => 'Artist',
            'value' => get_the_author_meta('display_name', $post->post_author),
            'icon' => 'artist',
            'url' => get_the_permalink(get_user_meta($post->post_author, BASE . 'link', true)),
         ];
         $this->cache->set($post->post_author, $author, 'author_data');
      }
      return $author;
      $author = $post->post_author;
      $userLink = get_user_meta($author, BASE.'profile_link', true);
      return $this->cache->remember(
         $userLink,
         function () use ($userLink, $author) {
            $label = jvbUserRole($author);
            $registrar = Registrar::getInstance($label);
            if ($registrar) {
               $label = $registrar->getSingular();
            } else {
               $label = 'Artist';
            }
            return [
               'id'  => $userLink,
               'label'  => $label,
               'value'  => get_the_title($userLink),
               'icon'   => 'user',
               'url' => get_the_permalink($userLink),
            ];
         }
      );
   }
   protected function getTaxonomies(int $postID, string $content): array
   {
      $taxonomies = jvbTaxonomiesForContent($content);
      $registrar = Registrar::getInstance($content)??false;
      $taxonomies = $registrar->registrar->taxonomies;
      $out = [];
      foreach ($taxonomies as $tax) {
         $terms = get_the_terms($postID, $tax);
         $t = [];
         if ($terms && !is_wp_error($terms)) {
            $config = jvbNoBase($tax);
            $config = Registrar::getInstance($tax);
            $out[] = [
               'icon' => $config,
               'title' => JVB_TAXONOMY[$config]['plural'],
               'icon' => $config->getIcon(),
               'title' => $config->getPlural(),
               'terms' => array_map(function ($term) use ($tax, $postID, $content) {
                  return [
                     'ID' => $term->term_id,
                     'title' => htmlspecialchars_decode($term->name),
                     'url' => get_term_link($term->term_id, $tax),
                     'umami_click' => $this->tracker->trackTaxonomyClick($term->term_id, $tax, [
                        'from' => $content . '_' . $postID
                     ])
                  ];
                  $item = $this->cache->remember(
                     $term->term_id,
                     function() use ($term, $tax, $content, $postID) {
                        return [
                           'ID'  => $term->term_id,
                           'title'  => html_entity_decode($term->name),
                           'url' => get_term_link($term->term_id, $tax),
                        ];
                     }
                  );
                  $item['umami_click'] = $this->tracker->trackTaxonomyClick($term->term_id, $tax, [
                     'from'   => $content.'_'.$postID
                  ]);
                  return $item;
               }, $terms),
            ];
         }
      }
      return $out;
@@ -336,11 +400,9 @@
   protected function buildRequestArgs(WP_REST_Request $request): array
   {
      $data = $request->get_params();
      error_log('Feed Request: ' . print_r($data, true));
      $args = [
         'post_type' => (array_key_exists($data['content'], $this->buildFeedTypesConfig())) ?
            BASE . $data['content'] :
            BASE . array_key_first(JVB_CONTENT),
            jvbCheckBase($data['content']) : null,
         'paged' => intval($data['page'] ?? 1),
         'posts_per_page' => $this->per_page,
      ];
@@ -353,52 +415,15 @@
            ]
         );
      }
      if (array_key_exists('taxonomy', $data) && is_string($data['taxonomy'])) {
         $data['taxonomy'] = json_decode($data['taxonomy'], true);
      }
      $args = $this->applyContextFilters($args, $data);
      $args = $this->applyTaxonomyFilters($args, $data);
      $args = $this->applyOrderFilters($args, $data);
      $args = $this->applyDateFilters($args, $data);
      $args = $this->applyFavouritesFilter($args, $data);
      error_log('Final Args: '.print_r($args, true));
      return $args;
   }
   protected function applyTaxonomyFilters(array $args, array $data): array
   {
      if (!isset($data['taxonomy']) || empty($data['taxonomy'])) {
         return $args;
      }
      $taxonomyFilters = $data['taxonomy'];
      // Validate taxonomies exist and sanitize
      $validFilters = [];
      foreach ($taxonomyFilters as $taxonomy => $terms) {
         if (!taxonomy_exists(jvbCheckBase($taxonomy))) {
            continue;
         }
         $validFilters[] = [
            'taxonomy' => jvbCheckBase($taxonomy),
            'field' => 'term_id',
            'terms' => array_map('absint', (array)$terms),
            'operator' => 'IN'
         ];
      }
      if (empty($validFilters)) {
         return $args;
      }
      // Determine relation based on match filter
      $relation = ($data['match'] ?? 'all') === 'all' ? 'AND' : 'OR';
      $args['tax_query'] = array_merge(
         ['relation' => $relation],
         $validFilters
      );
      return $args;
      return $this->applyFavouritesFilter($args, $data);
   }
   /**
@@ -409,19 +434,17 @@
   public function handleFeedRequest(WP_REST_Request $request): WP_REST_Response
   {
      $args = $this->buildRequestArgs($request);
      $cacheContext = $this->buildCacheContext($args, $request);
      $key = $this->cache->generateKey($args);
      // Check HTTP cache headers first
      $cache_check = $this->checkHeaders(
         $request,
         $cacheContext['content_types'],
         $cacheContext['additional_params']
         $key
      );
      if ($cache_check) {
         return $cache_check; // Returns 304 Not Modified
      }
      error_log('Feed Request Args: '.print_r($args, true));
      $key = $this->cache->generateKey($args);
      $cached = $this->cache->get($key);
      if ($cached) {
         if ($request->get_param('highlight')) {
@@ -429,16 +452,13 @@
            $args['highlight'] = $highlight;
         }
         $cached['items'] = $this->processHighlightedItem($cached['items'], $args);
         $response = new WP_REST_Response($cached);
         $response = $this->success($cached);
         return $this->addCacheHeaders($response);
      }
      // Fetch and format items
      $items = $this->fetchFeedItems($args);
      error_log('Feed Got items: ' .print_r($items, true));
      $ttl = (str_contains($args['orderby'], 'RAND')) ? 1800 : $this->cache_ttl;
      $ttl = (str_contains($args['orderby'], 'RAND')) ? 300 : $this->cacheTtl;
      $this->cache->set($key, $items, $ttl);
      if ($request->get_param('highlight')) {
@@ -447,93 +467,18 @@
      }
      $items['items'] = $this->processHighlightedItem($items['items'], $args);
      $response = new WP_REST_Response($items);
      $response = $this->success($items);
      return $this->addCacheHeaders($response);
   }
   /**
    * Build cache context from query args
    * Extracts content types and parameters needed for proper cache checking
    *
    * @param array $args Built WP_Query arguments
    * @param WP_REST_Request $request Original request
    * @return array Cache context with content_types and additional_params
    */
   protected function buildCacheContext(array $args, WP_REST_Request $request): array
   {
      // Extract content types from post_type in args
      $post_types = is_array($args['post_type'])
         ? $args['post_type']
         : [$args['post_type']];
      $content_types = array_map('jvbNoBase', $post_types);
      $content_types[] = 'feed'; // Always include base feed type
      // Build additional params for ETag uniqueness
      $additional_params = [
         'order' => $args['orderby'] ?? 'date',
         'direction' => $args['order'] ?? 'DESC',
         'page' => $args['paged'] ?? 1,
      ];
      if ($request->get_param('favourites')) {
         $additional_params['user'] = (int)$request->get_param('user');
      }
      // Include author filter if present (from context or favourites)
      if (!empty($args['author'])) {
         $additional_params['author'] = $args['author'];
      }
      if (!empty($args['author__in'])) {
         $additional_params['author__in'] = $args['author__in'];
      }
      // Include taxonomy filters if present
      if (!empty($args['tax_query'])) {
         $tax_filters = [];
         foreach ($args['tax_query'] as $key => $query) {
            if ($key === 'relation' || !is_array($query)) {
               continue;
            }
            $taxonomy = jvbNoBase($query['taxonomy'] ?? '');
            if ($taxonomy) {
               $tax_filters[$taxonomy] = $query['terms'] ?? [];
               // Also add taxonomy to content_types for timestamp checking
               $content_types[] = $taxonomy;
            }
         }
         if (!empty($tax_filters)) {
            $additional_params['taxonomies'] = $tax_filters;
         }
      }
      // Include date filters if present
      if (!empty($args['date_query'])) {
         $additional_params['date_filter'] = md5(serialize($args['date_query']));
      }
      // Include meta queries if present
      if (!empty($args['meta_query'])) {
         $additional_params['meta_filter'] = md5(serialize($args['meta_query']));
      }
      return [
         'content_types' => array_unique($content_types),
         'additional_params' => $additional_params
      ];
   }
   /**
    * @param array $args Formatted Args for WP_Query
    * @param array $items Formatted Args for WP_Query
    * @param array $data parsed Request Data
    *
    * @return array|null
    */
   protected function processHighlightedItem(array $items, array $data): array
   {
      error_log('Data passed to processHighlightedItem:' . print_r($data, true));
      if (empty($data['highlight'] ?? null)) {
         return $items;
      }
@@ -546,15 +491,8 @@
      // Extract key and value
      $key = array_keys($data['highlight'])[0] ?? false;
      $value = array_values($data['highlight'])[0] ?? false;
      error_log('Highlighted item: ' . $key);
      error_log('Highlighted item: ' . $value);
      error_log('No Single Content Types: ' . print_r(jvbNoSingleContentTypes(), true));
      error_log('Page: ' . print_r($data['paged'], true));
      if (in_array($key, jvbNoSingleContentTypes()) && $value && $data['paged'] === 1) {
         error_log('Formatted Highlighted item: ' . print_r($this->formatItem($value), true));
         error_log('Items: ' . print_r($items, true));
      if ($key && $data['paged'] === 1) {
         array_unshift($items, $this->formatItem($value));
         error_log('Items after unshift: ' . print_r($items, true));
      }
      return $items;
   }
@@ -571,45 +509,41 @@
         return $args;
      }
      $registrar = Registrar::getInstance($context['type']);
      switch (true) {
         case contentIsJVBUserType($context['type']):
         case $registrar->hasFeature('profile_link'):
            $args['author'] = (int)get_post_meta($context['id'], BASE . 'link', true);
            break;
         case taxIsJVBContentTax($context['type']):
         case $registrar->getType() === 'term' && $registrar->hasFeature('is_content'):
            $args['post_type'] = is_array($args['post_type'])
               ? $args['post_type']
               : explode(',', $args['post_type']);
            // Check if filtering global feed content
            $globalFeedTypes = array_map('jvbCheckBase',
               array_keys(Features::getTypesWithFeature('show_feed', 'content'))
            );
            if (in_array(jvbNoBase($context['type']), Registrar::getFeatured('is_content', 'term'))) {
               // Global: show posts from any content type with this taxonomy
               $for_content = Registrar::getInstance($context['type'])->registrar->for ?? [];
            if (array_intersect($args['post_type'], $globalFeedTypes)) {
               $artists = jvbGetContentUsers($context['id']);
               if (!empty($artists)) {
                  $args['author__in'] = $artists;
               }
            } else {
               $args['tax_query'] = [
                  'relation' => 'AND',
                  [
                     'taxonomy' => BASE . $context['type'],
                     'terms' => $context['id'],
                  ]
               ];
               // Convert to full post types with BASE prefix
               $post_types = array_map(fn($type) => jvbCheckBase($type), $for_content);
               // Filter to only show_feed content types
               $show_feed_types = Registrar::getFeatured('show_feed', 'post');
               $args['post_type'] = array_intersect(
                  $post_types,
                  array_map(fn($type) => jvbCheckBase($type), $show_feed_types)
               );
            }
            break;
         case taxonomy_exists(jvbCheckBase($context['type'])):
            $args['tax_query'] = [
               'relation' => 'AND',
               [
                  'taxonomy' => BASE . $context['type'],
                  'terms' => $context['id'],
               ]
            // Add term to tax query
            $args['tax_query'][] = [
               'taxonomy' => jvbCheckBase($context['type']),
               'field' => 'term_id',
               'terms' => [(int)$context['id']],
            ];
            break;
      }
      return $args;
   }
@@ -619,40 +553,34 @@
    *
    * @return array
    */
   protected function applyFavouritesFilter(array $args, array $filters): array
   protected function applyFavouritesFilter(array $args, array $data): array
   {
      if (!array_key_exists('favourites', $filters)) {
      if (empty($data['favourites']) || empty($data['user'])) {
         return $args;
      }
      error_log('Proceeding to check for favourites:');
      global $wpdb;
      // Get post types for the current filter
      $post_types = is_array($args['post_type'])
         ? $args['post_type']
         : [$args['post_type']];
      $user_id = (int)$data['user'];
      $content = jvbNoBase($args['post_type']);
      $favourites_table = $wpdb->prefix . BASE . 'favourites';
      $placeholders = implode(',', array_fill(0, count($post_types), '%s'));
      error_log('CurrentUser ID: ' . print_r(get_current_user_id(), true));
      $favourited_ids = $wpdb->get_col($wpdb->prepare(
         "SELECT target_id FROM {$favourites_table}
            WHERE user_id = %d AND type IN ($placeholders)",
         array_merge(
            [get_current_user_id()],
            $post_types
         )
      ));
      // Get user's favourites for this content type
      $fav_key = BASE . 'favourites_' . $content;
      $favourites = get_user_meta($user_id, $fav_key, true);
      if (empty($favourited_ids)) {
         // Force empty results
      if (empty($favourites)) {
         // No favourites - return empty result
         $args['post__in'] = [0]; // Will return no results
         return $args;
      }
      $fav_ids = array_filter(array_map('intval', explode(',', $favourites)));
      if (empty($fav_ids)) {
         $args['post__in'] = [0];
         return $args;
      }
      $args['post__in'] = isset($args['post__in'])
         ? array_intersect($args['post__in'], $favourited_ids)
         : $favourited_ids;
      $args['post__in'] = $fav_ids;
      $args['orderby'] = 'post__in'; // Preserve favourite order
      return $args;
   }
@@ -666,11 +594,11 @@
   {
      $postType = is_array($args['post_type']) ? $args['post_type'][0] : $args['post_type'];
      $slug = jvbNoBase($postType);
      if (Features::forContent($slug)->has('is_timeline')) {
      $registrar = Registrar::getInstance($slug);
      if ($registrar && $registrar->hasFeature('is_timeline')) {
         $args['post_parent'] = 0;
      }
      if (in_array($slug, Features::getTypesWithFeature('is_content', 'taxonomy'))) {
      if ($registrar && $registrar->hasFeature('is_content')) {
         return $this->handleContentTaxonomies($args);
      }
      $args['fields'] = 'ids';
@@ -1197,15 +1125,6 @@
      }
   }
   /**
    * Get custom table fields for a taxonomy
    * @param string $taxonomy Taxonomy type
    * @return array Field definitions
    */
   protected function getCustomTableFields(string $taxonomy): array
   {
      return jvbContentTaxonomiesTableFields($taxonomy)['fields'] ?? [];
   }
   /**
    * Get available feed types (for block editor)
@@ -1214,14 +1133,14 @@
   public function getFeedTypes(WP_REST_Request $request): WP_REST_Response
   {
      // Check HTTP cache
      $cache_check = $this->checkHeaders($request, ['feed_types']);
      $cache_check = $this->checkHeaders($request, 'feed_types');
      if ($cache_check) {
         return $cache_check;
      }
      $feedTypes = $this->buildFeedTypesConfig();
      $response = new WP_REST_Response($feedTypes);
      $response = $this->success($feedTypes);
      return $this->addCacheHeaders($response);
   }
@@ -1234,44 +1153,44 @@
    */
   protected function buildFeedTypesConfig(): array
   {
      if (!$this->checker) {
         $this->checker = Checker::getInstance();
      }
      return $this->cache->remember(
         'contentTypes',
         function () {
            $config = [];
            // Get content types with show_feed
            $contentTypes = Features::getTypesWithFeature('show_feed', 'content');
            $contentTypes = Registrar::getFeatured('show_feed', 'post');
            foreach ($contentTypes as $slug) {
               $contentConfig = JVB_CONTENT[$slug] ?? null;
               if (!$contentConfig) continue;
               $this->cache->tag('content:'.$slug);
               $registrar = Registrar::getInstance($slug);
               if (!$registrar) continue;
               $config[$slug] = [
                  'type' => 'content',
                  'singular' => $contentConfig['singular'] ?? ucfirst($slug),
                  'plural' => $contentConfig['plural'] ?? ucfirst($slug) . 's',
                  'icon' => $slug,
                  'taxonomies' => $this->checker->getTaxonomiesForContent($slug),
                  'singular' => $registrar->getSingular(),
                  'plural' => $registrar->getPlural(),
                  'icon' => $registrar->getIcon(),
                  'taxonomies' => $registrar->registrar->taxonomies,
               ];
            }
            // Get taxonomies with show_feed (content taxonomies)
            $taxonomies = Features::getTypesWithFeature('show_feed', 'taxonomy');
            $taxonomies = Registrar::getFeatured('show_feed', 'term');
            foreach ($taxonomies as $slug) {
               $taxConfig = JVB_TAXONOMY[$slug] ?? null;
               if (!$taxConfig || !($taxConfig['is_content'] ?? false)) {
               $registrar = Registrar::getInstance($slug);
               if (!$registrar || !($registrar->hasFeature('is_content') ?? false)) {
                  continue;
               }
               $this->cache->tag('taxonomy:'.$slug);
               $config[$slug] = [
                  'type' => 'taxonomy',
                  'singular' => $taxConfig['singular'] ?? ucfirst($slug),
                  'plural' => $taxConfig['plural'] ?? ucfirst($slug) . 's',
                  'icon' => $slug,
                  'singular' => $registrar->getSingular(),
                  'plural' => $registrar->getPlural(),
                  'icon' => $registrar->getIcon(),
                  'taxonomies' => [], // Content taxonomies don't have sub-taxonomies
                  'for_content' => $taxConfig['for_content'] ?? [],
                  'for_content' => $registrar->registrar->for ?? []
               ];
            }