From 48721c85ebcfa973ee81719d2467ca80e4253dc9 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 01 May 2026 17:30:03 +0000
Subject: [PATCH] =Edmonton Ink hard test begins! Real testing of the managers and reset routes will commence. So far, just ensuring our classes are all loaded correctly: Site() and its sub-classes Membership, Login, etc. Care should be taken to load conditionally on 'init', as we finish defining most settings by 'plugins_loaded' at priority 5

---
 inc/rest/routes/FeedRoutes.php | 1921 +++++++++++++++++++++++++++++++--------------------------
 1 files changed, 1,052 insertions(+), 869 deletions(-)

diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index eb7024e..4efc099 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/inc/rest/routes/FeedRoutes.php
@@ -1,10 +1,12 @@
 <?php
 namespace JVBase\rest\routes;
 
-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\rest\Route;
+use JVBase\base\Site;
 use WP_Query;
 use WP_Post;
 use WP_Term;
@@ -14,1004 +16,1185 @@
 if (!defined('ABSPATH')) {
     exit; // Exit if accessed directly
 }
-/**
- * FeedRoutes - Optimized API endpoints for the feed block
- * TODO: Look at Content Routes' setup and make this more like that one; it's a bit more organized
- * Or NewsRoutes
- */
-class FeedRoutes extends RestRouteManager
+
+class FeedRoutes extends Rest
 {
-    protected int $per_page = 36;
-    protected Umami $tracker;
+	protected int $per_page = 36;
+	protected ?Umami $tracker = null;
 
-    public function __construct()
-    {
-        $this->cache_name = 'feed';
-        $this->cache_ttl = 86400;
+	protected ?array $fields = null;
+	protected ?array $timelineSharedFields = null;
+	protected ?array $timelineUniqueFields = null;
 
-        if (jvbSiteUsesUmami()) {
-            $this->tracker = JVB()->connect('umami');
-        }
-        parent::__construct();
-    }
+	public function __construct()
+	{
+		$this->cacheName = 'feed';
+		$this->cacheTtl = 86400;
+		parent::__construct();
+		$this->cache
+			->connect('post', true)
+			->connect('taxonomy', true)
+			->connect('user', true);
 
-    /**
-     * Registers feed routes
-     * @return void
-     */
-    public function registerRoutes():void
-    {
-        register_rest_route($this->namespace, '/feed', [
-            'methods' => ['GET', 'POST'],
-            'callback' => [$this, 'handleFeedRequest'],
-            'permission_callback' => [$this, 'checkPermission'],
-        ]);
-    }
+		if (JVB_TESTING) {
+			$this->cache->flush();
+		}
 
-    /**
-     * Formats an item
-     * @param int $postID
-     *
-     * @return array
-     */
-    protected function formatItem(int $postID, string $type = 'post'):array
-    {
-        switch ($type) {
-            case 'post':
-                $post = get_post($postID);
-                $type = jvbNoBase($post->post_type);
-                $metaType = 'post';
-                break;
-            default:
-                $post = get_term($postID, jvbCheckBase($type));
-                $type = jvbNoBase($type);
-                $metaType = 'term';
-                break;
-        }
-        if (!$post || is_wp_error($post)) {
-            return [];
-        }
-        $formatted = $this->cache->get($postID, $type);
-//        $formatted = false;
-        if ($formatted) {
-            return $formatted;
-        }
+	}
+
+	public function init():void
+	{
+		if (Site::hasIntegration('umami')) {
+			$this->tracker = JVB()->connect('umami');
+		}
+	}
+
+	/**
+	 * Registers feed routes
+	 * @return void
+	 */
+	public function registerRoutes(): void
+	{
+		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();
+
+		// Feed types endpoint
+		Route::for('feed/types')
+			->get([$this, 'getFeedTypes'])
+			->auth('public')
+			->rateLimit()
+			->register();
+	}
+
+	/**
+	 * Formats an item
+	 * @param int $postID
+	 *
+	 * @return array
+	 */
+	protected function formatItem(int $postID, string $type = 'post', $skip = false): array
+	{
+		switch ($type) {
+			case 'post':
+				$post = get_post($postID);
+				$type = jvbNoBase($post->post_type);
+				$metaType = 'post';
+				break;
+			default:
+				$post = get_term($postID, jvbCheckBase($type));
+				$type = jvbNoBase($type);
+				$metaType = 'term';
+				break;
+		}
+		if (!$post || is_wp_error($post)) {
+			return [];
+		}
+
+		return $this->cache->remember(
+			$postID,
+			function() use ($postID, $type, $metaType, $post, $skip) {
+				$registrar = null;
+				switch ($metaType) {
+					case 'post':
+						$registrar = Registrar::getInstance($type);
+						$meta = Meta::forPost($postID);
+						if (!$skip && $registrar->isTimeline()) {
+							return $this->formatTimeline($postID, $post);
+						}
+						break;
+					case 'term':
+
+						$meta = Meta::forTerm($postID);
+						$registrar = Registrar::getInstance($type);
+						break;
+					case 'user':
+						$meta = Meta::forUser($postID);
+						$registrar = Registrar::getInstance($type);
+						break;
+				}
+				if (!$registrar) {
+					return [];
+				}
+				$fields = $registrar->getFields();
+
+				//Allow custom filtering for public 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);
+				}
+
+				$values = $meta->getAll(array_keys($fields));
+
+				$out = [
+					'fields' => $values,
+				];
+
+				//Format Taxonomies
+				$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['fields']) && $out['fields'][$key] !== '') {
+						$IDs = array_map('absint', explode(',',$out['fields'][$key]));
+						foreach ($IDs as $ID) {
+							$imgIDs[$ID] = jvbImageData($ID);
+						}
+					}
+				}
+				$out['images'] = $imgIDs;
+
+				$out['id'] = $postID;
+				$out['content'] = $type;
+				$out['icon'] = $registrar->getIcon()??jvbDefaultIcon();
+				if ($out['icon'] === '') {
+					$out['icon'] = jvbDefaultIcon();
+				}
+
+				if ($this->tracker) {
+					$args = ($metaType === 'post') ? ['owner_id' => $post->post_author] : [];
+					$out['umami_view'] = $this->tracker->trackFeedView($postID, $type, $args);
+					$out['umami_fav'] = $this->tracker->trackFavouriteToggle($postID, $type, false);
+					$out['umami_click'] = $this->tracker->trackClick($postID, $type);
+				}
+
+				switch ($metaType) {
+					case 'term':
+
+						$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;
+			}
+		);
+	}
 
 
-        $fields = apply_filters(
-            'jvbFeedFields',
-            [],
-            $type
-        );
 
-        $meta = new MetaManager($postID, $metaType);
-        $formatted = [
-            'id'    => $postID,
-            'icon'  => $type,
-        ];
+	protected function initTimelineFields(string $content):void
+	{
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || !$registrar->hasFeature('is_timeline')){
+			return;
+		}
+		$this->fields = $registrar->getFields();
 
-        if (jvbSiteUsesUmami()) {
-            $args = ($metaType === 'post') ? [ 'owner_id' => $post->post_author] : [];
-            $formatted['umami_view'] = $this->tracker->trackFeedView($postID, $type, $args);
-            $formatted['umami_fav'] = $this->tracker->trackFavouriteToggle($postID, $type, false);
-        }
-        switch ($metaType) {
-            case 'term':
-                if (jvbSiteUsesUmami()) {
-                    $formatted['umami_click'] = $this->tracker->trackTaxonomyClick($postID, $type);
-                }
-                $owner = (in_array($type, jvbContentTaxonomies()) ? $meta->getValue('owner') : null);
-                if (!is_null($owner)) {
-                    $formatted['user_id'] = $owner;
-                }
-                $formatted['url'] = get_term_link($postID, $type);
-                break;
-            default:
-                $formatted = array_merge($formatted, [
-                    'date'      => $post->post_date,
-                    'user_id'   => (int)$post->post_author,
-                    'url'       => get_the_permalink($postID),
-                ]);
-                break;
-        }
-        $order = array_keys($fields);
-        foreach ($fields as $field => $config) {
-            $value = [];
-            if ($field === 'umami_click') {
-                if ($config === 'profile') {
-                    $formatted['umami_click'] = $this->tracker->trackProfileClick($postID, $type);
-                }
-                if ($config === 'contentTaxonomy') {
-                    $formatted['umami_click'] = $this->tracker->trackContentTaxonomyClick($postID, $type);
-                }
-            } else {
-                if (array_key_exists('field', $config)) {
-                    if ($field === 'image' && array_key_exists('gallery', $config)) {
-                        //Array === post types
-                        if (is_array($config['gallery'])) {
-                            $posts = get_posts([
-                                'post_type' => array_map(function ($item) { return BASE.$item; }, $config['gallery']),
-                                'author'    => $post->post_author,
-                                'posts_per_page'    => 5,
-                                'orderby'   => 'date',
-                                'order'     => 'DESC',
-                            ]);
-                            $formatted['content'] = array_map(function ($content) {
-                                return [
-                                    'url'   => get_permalink($content->ID),
-                                    'title' => $content->post_title,
-                                    'image' => jvbImageData((int)get_post_thumbnail_id($content->ID)),
-                                ];
-                            }, $posts);
-                        } else {
-                            //String === $meta
-                            $ids = explode(',', $meta->getValue($config['gallery']));
-                            $formatted['content'] = array_map(function ($id) {
-                                return [
-                                    'image' => jvbImageData((int)$id)
-                                ];
-                            }, $ids);
-                        }
-                    }
-                    switch ($config['field']) {
-                        case 'post_author':
-                            $author = $this->getAuthorData($post);
-                            $value = [
-                                'value' => $author['value'],
-                                'url'   => $author['url']
-                            ];
-                            break;
-                        case 'name':
-                            $value = $post->name;
-                            break;
-                        case 'post_title':
-                            $value = $post->post_title;
-                            break;
-                        case 'image':
-                        case 'image_portrait':
-                        case 'featured_image':
-                            $value = $meta->getValue($config['field']);
-                            $value =  jvbImageData((int)$value);
-                            break;
-                        case 'top_style':
-                        case 'city':
-                        case 'top_theme':
-                            $terms = explode(',', $meta->getValue($field));
-                            $terms = array_filter(array_map(function ($termID) use ($config, $postID, $type) {
-                                $term = get_term($termID, jvbCheckBase($config['icon']));
-                                if ($term && !is_wp_error($term)) {
-                                    return $this->formatTaxonomy($term, $postID, $type);
-                                }
-                                return [];
-                            }, $terms));
-                            $value = [
-                                'terms' => $terms
-                            ];
-                            break;
-                        default:
-                            $value = [
-                                'value' => $meta->getValue($field)
-                            ];
-                    }
+		$this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
+			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
+				return true;
+			}
+			return false;
+		}));
+		array_unshift($this->timelineSharedFields, 'post_thumbnail');
+		array_unshift($this->timelineSharedFields, 'post_title');
 
-                } elseif (array_key_exists('taxonomy', $config)) {
-                    $terms = get_the_terms($postID, BASE.$config['taxonomy']);
-                    if ($terms && !is_wp_error($terms)) {
-                        $terms = array_map(function ($term) use ($postID, $type) {
-                            return $this->formatTaxonomy($term, $postID, $type);
-                        }, $terms);
-                        $value = [
-                            'terms'     => $terms
-                        ];
-                    }
-                }
+		$this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
+			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
+				return true;
+			}
+			return false;
+		}));
+	}
 
-                if (array_key_exists('label', $config)) {
-                    $value['label'] = $config['label'];
-                }
-                if (array_key_exists('icon', $config)) {
-                    $value['icon'] = $config['icon'];
-                }
-                $formatted[$field] = $value;
-            }
-        }
+	protected function formatTimeline(int $postID, WP_Post $post):array
+	{
+		if (!$this->timelineSharedFields || !$this->timelineUniqueFields){
+			$this->initTimelineFields($post->post_type);
+		}
+		$item = $this->formatItem($postID, 'post', true);
+		//Step 1: Get the fields that apply to all posts
+		$mainMeta = Meta::forPost($post->ID);
+		$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
 
-        $formatted['order'] = $order;
-        $this->cache->set($postID, $formatted, $type);
+		//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);
 
-        return $formatted;
-    }
+		$item['taxonomies'] = $this->extractTaxonomies($item['fields'], $postID, jvbNoBase($post->post_type));
 
-    protected function formatTaxonomy(WP_Term $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
-            ])
-        ];
-    }
-    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;
-    }
+		$subFields = [];
+		$images = [];
+		foreach ($children as $child) {
+			$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['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)]);
 
-    protected function getTaxonomies(int $postID, string $content):array
-    {
-        $taxonomies = jvbTaxonomiesForContent($content);
-        $out = [];
-        foreach ($taxonomies as $tax) {
-            $terms = get_the_terms($postID, $tax);
-            $t = [];
-            if ($terms && !is_wp_error($terms)) {
-                $config = jvbNoBase($tax);
-                $out[] = [
-                    'icon'  => $config,
-                    'title' => JVB_TAXONOMY[$config]['plural'],
-                    '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
-                            ])
-                        ];
-                    }, $terms),
-                ];
-            }
-        }
-        return $out;
-    }
+		$item['fields']['timeline'] = $subFields;
+		$item['images'] = $item['images'] + $images;
 
 
-    protected function buildRequestArgs(WP_REST_Request $request):array
-    {
-        global $jvb_everything;
-        $data = $request->get_params();
-        error_log('Feed Request: '.print_r($data, true));
-        $args = [
-            'post_type'         => (array_key_exists($data['content'], $jvb_everything)) ?
-                BASE.$data['content'] :
-                BASE.array_key_first(JVB_CONTENT),
-            'paged'             => intval($data['page'] ?? 1),
-            'posts_per_page'    => $this->per_page,
-        ];
-        if (!empty($data['context'])) {
-            $args = $this->applyContextFilters(
-                $args,
-                [
-                    'id' => $data['source'],
-                    'type'=>$data['context']
-                ]
-            );
-        }
-        $args = $this->applyContextFilters($args, $data);
-        $args = $this->applyTaxonomyFilters($args, $data);
-        $args = $this->applyOrderFilters($args, $data);
-        $args = $this->applyDateFilters($args, $data);
+		return $item;
+	}
+	protected function extractTaxonomies(array $fields, int $postID, string $content):array {
+		$taxonomies = [];
+		foreach ($fields as $key => $value) {
+			if (empty($value)) {
+				continue;
+			}
 
-        $args = $this->applyFavouritesFilter($args, $data);
+			$registrar = Registrar::getInstance($key);
+			if (!$registrar || $registrar->registrar->public === false){
+				continue;
+			}
 
-        return $args;
-    }
-    /**
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleFeedRequest(WP_REST_Request $request):WP_REST_Response
-    {
-        $args = $this->buildRequestArgs($request);
+			$terms = array_map('absint', explode(',', $value));
+			$terms = array_filter($terms); // Remove 0 values
 
-        error_log('Final Args: '.print_r($args, true));
+			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 $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 = $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
+	{
+		$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 = Registrar::getInstance($tax);
+				$out[] = [
+					'icon' => $config->getIcon(),
+					'title' => $config->getPlural(),
+					'terms' => array_map(function ($term) use ($tax, $postID, $content) {
+						$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;
+	}
 
 
-        $key = $this->cache->generateKey($args);
-        $cached = $this->cache->get($key);
-        if ($cached) {
-            if ($request->get_param('highlight')) {
-                $highlight = json_decode($request->get_param('highlight'), true);
-                $args['highlight'] = $highlight;
-            }
-            $cached['items'] = $this->processHighlightedItem($cached['items'], $args);
-            return new WP_REST_Response($cached);
-        }
-        // Fetch and format items
-        $items = $this->fetchFeedItems($args);
+	protected function buildRequestArgs(WP_REST_Request $request): array
+	{
+		$data = $request->get_params();
+		$args = [
+			'post_type' => (array_key_exists($data['content'], $this->buildFeedTypesConfig())) ?
+				jvbCheckBase($data['content']) : null,
+			'paged' => intval($data['page'] ?? 1),
+			'posts_per_page' => $this->per_page,
+		];
+		if (!empty($data['context'])) {
+			$args = $this->applyContextFilters(
+				$args,
+				[
+					'id' => $data['source']??'0',
+					'type' => $data['context']
+				]
+			);
+		}
+		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);
 
+		return $this->applyFavouritesFilter($args, $data);
+	}
 
-        $ttl = (str_contains($args['orderby'], 'RAND')) ? 1800 : $this->cache_ttl;
-        $this->cache->set($key, $items, $ttl);
+	/**
+	 * @param WP_REST_Request $request
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function handleFeedRequest(WP_REST_Request $request): WP_REST_Response
+	{
+		$args = $this->buildRequestArgs($request);
+		$key = $this->cache->generateKey($args);
 
-        if ($request->get_param('highlight')) {
-            $highlight = json_decode($request->get_param('highlight'), true);
-            $args['highlight'] = $highlight;
-        }
+		// Check HTTP cache headers first
+		$cache_check = $this->checkHeaders(
+			$request,
+			$key
+		);
+		if ($cache_check) {
+			return $cache_check; // Returns 304 Not Modified
+		}
 
-        $items['items'] = $this->processHighlightedItem($items['items'], $args);
-        return new WP_REST_Response($items);
-    }
+		$cached = $this->cache->get($key);
+		if ($cached) {
+			if ($request->get_param('highlight')) {
+				$highlight = json_decode($request->get_param('highlight'), true);
+				$args['highlight'] = $highlight;
+			}
+			$cached['items'] = $this->processHighlightedItem($cached['items'], $args);
+			$response = $this->success($cached);
+			return $this->addCacheHeaders($response);
+		}
+		// Fetch and format items
+		$items = $this->fetchFeedItems($args);
 
-    /**
-     * @param array $args 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;
-        }
+		$ttl = (str_contains($args['orderby'], 'RAND')) ? 300 : $this->cacheTtl;
+		$this->cache->set($key, $items, $ttl);
 
-        // Convert to array if string
-        if (is_string($data['highlight'])) {
-            $data['highlight'] = json_decode($data['highlight'], true);
-        }
+		if ($request->get_param('highlight')) {
+			$highlight = json_decode($request->get_param('highlight'), true);
+			$args['highlight'] = $highlight;
+		}
 
-        // 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));
-            array_unshift($items, $this->formatItem($value));
-            error_log('Items after unshift: '.print_r($items, true));
-        }
-        return $items;
-    }
+		$items['items'] = $this->processHighlightedItem($items['items'], $args);
+		$response = $this->success($items);
+		return $this->addCacheHeaders($response);
+	}
 
-    /**
-     * @param array $args
-     * @param array $context
-     *
-     * @return array
-     */
-    protected function applyContextFilters(array $args, array $context):array
-    {
+	/**
+	 * @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
+	{
+		if (empty($data['highlight'] ?? null)) {
+			return $items;
+		}
 
-        error_log('Args at Context Filters: '.print_r($args, true));
-        error_log('Request at Context Filters: '.print_r($context, true));
-        if (!isset($context['type'])) {
-            return $args;
-        }
+		// Convert to array if string
+		if (is_string($data['highlight'])) {
+			$data['highlight'] = json_decode($data['highlight'], true);
+		}
 
-        switch (true) {
-            case contentIsJVBUserType($context['type']):
-                $args['author'] = (int)get_post_meta($context['id'], BASE.'link', true);
-                break;
-            case taxIsJVBContentTax($context['type']):
-                $args['post_type'] = (is_array($args['post_type'])) ? $args['post_type'] : explode(',',$args['post_type']);
-                if (array_intersect($args['post_type'], array_map(function ($type) { return jvbCheckBase($type); },array_keys(jvbGlobalFeedContent())))) {
-                    $artists = jvbGetContentUsers($context['id']);
-                    if (!empty($artists)) {
-                        $args['author__in'] = $artists;
-                    }
-                } else {
-                    $args['tax_query'] = [
-                        'relation'  => 'AND',
-                        [
-                            'taxonomy' => BASE.$context['type'],
-                            'terms' => $context['id'],
-                        ]
-                    ];
-                }
-                break;
-            case taxonomy_exists(jvbCheckBase($context['type'])):
-                $args['tax_query'] = [
-                    'relation'  => 'AND',
-                    [
-                        'taxonomy' => BASE.$context['type'],
-                        'terms' => $context['id'],
-                    ]
-                ];
-                break;
-        }
-        return $args;
-    }
+		// Extract key and value
+		$key = array_keys($data['highlight'])[0] ?? false;
+		$value = array_values($data['highlight'])[0] ?? false;
+		if ($key && $data['paged'] === 1) {
+			array_unshift($items, $this->formatItem($value));
+		}
+		return $items;
+	}
 
-    /**
-     * @param array $args
-     * @param array $filters
-     *
-     * @return array
-     */
-    protected function applyFavouritesFilter(array $args, array $filters):array
-    {
-        if (!array_key_exists('favourites', $filters)){
-            return $args;
-        }
-        error_log('Proceeding to check for favourites:');
-        global $wpdb;
+	/**
+	 * @param array $args
+	 * @param array $context
+	 *
+	 * @return array
+	 */
+	protected function applyContextFilters(array $args, array $context): array
+	{
+		if (!isset($context['type'])) {
+			return $args;
+		}
 
-        // Get post types for the current filter
-        $post_types = explode(',', $args['post_type']);
+		$registrar = Registrar::getInstance($context['type']);
+		switch (true) {
+			case $registrar->hasFeature('profile_link'):
+				$args['author'] = (int)get_post_meta($context['id'], BASE . 'link', true);
+				break;
+			case $registrar->getType() === 'term' && $registrar->hasFeature('is_content'):
+				$args['post_type'] = is_array($args['post_type'])
+					? $args['post_type']
+					: explode(',', $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
-            )
-        ));
+				// Check if filtering global 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 (empty($favourited_ids)) {
-            // Force empty results
-            $args['post__in'] = [0];
-            return $args;
-        }
+					// Convert to full post types with BASE prefix
+					$post_types = array_map(fn($type) => jvbCheckBase($type), $for_content);
 
-        $args['post__in'] = isset($args['post__in'])
-            ? array_intersect($args['post__in'], $favourited_ids)
-            : $favourited_ids;
+					// 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)
+					);
+				}
 
-        return $args;
-    }
+				// Add term to tax query
+				$args['tax_query'][] = [
+					'taxonomy' => jvbCheckBase($context['type']),
+					'field' => 'term_id',
+					'terms' => [(int)$context['id']],
+				];
+				break;
+		}
 
-    /**
-     * @param array $args
-     *
-     * @return array
-     */
-    protected function fetchFeedItems(array $args):array
-    {
-        if (in_array($args['post_type'], jvbContentTaxonomies())) {
-            return $this->handleContentTaxonomies($args);
-        }
-        $args['fields'] = 'ids';
-        // Get post IDs
-        $query = new WP_Query($args);
+		return $args;
+	}
 
-        // Batch prefetch related data
-        update_meta_cache('post', $query->posts);
-        update_object_term_cache($query->posts, $args['post_type']);
+	/**
+	 * @param array $args
+	 * @param array $filters
+	 *
+	 * @return array
+	 */
+	protected function applyFavouritesFilter(array $args, array $data): array
+	{
+		if (empty($data['favourites']) || empty($data['user'])) {
+			return $args;
+		}
 
-        // Format regular items
-        $items = array_map(function ($post) {
-            return $this->formatItem($post);
-        }, $query->posts);
+		$user_id = (int)$data['user'];
+		$content = jvbNoBase($args['post_type']);
 
-        wp_reset_postdata();
-        return [
-            'items' => $items,
-            'has_more' => $query->max_num_pages > $args['paged'],
-            'total' => $query->found_posts
-        ];
-    }
+		// Get user's favourites for this content type
+		$fav_key = BASE . 'favourites_' . $content;
+		$favourites = get_user_meta($user_id, $fav_key, true);
 
-    protected function handleContentTaxonomies(array $args):array
-    {
+		if (empty($favourites)) {
+			// No favourites - return empty result
+			$args['post__in'] = [0]; // Will return no results
+			return $args;
+		}
 
-        $taxonomy = jvbNoBase($args['post_type']);
-        global $wpdb;
-        $table = $wpdb->prefix.BASE.'content_'.$taxonomy;
+		$fav_ids = array_filter(array_map('intval', explode(',', $favourites)));
 
-        // Check if table exists
-        if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) {
-            return [
-                'items' => [],
-                'has_more' => false,
-                'total' => 0
-            ];
-        }
+		if (empty($fav_ids)) {
+			$args['post__in'] = [0];
+			return $args;
+		}
 
-        // Build the query components
-        $queryBuilder = $this->buildCustomTableQuery($args, $table, $taxonomy);
+		$args['post__in'] = $fav_ids;
+		$args['orderby'] = 'post__in'; // Preserve favourite order
 
-        // Execute count query first
-        $total = (int) $wpdb->get_var($queryBuilder['count_query']);
+		return $args;
+	}
 
-        // Execute main query if we have results
-        $items = [];
-        if ($total > 0) {
-            $results = $wpdb->get_results($queryBuilder['main_query'],ARRAY_A);
-            $items = array_map(function ($ID) use ($taxonomy) {
-                return $this->formatItem($ID['term_id'], $taxonomy);
-            }, $results);
-        }
+	/**
+	 * @param array $args
+	 *
+	 * @return array
+	 */
+	protected function fetchFeedItems(array $args): array
+	{
+		$postType = is_array($args['post_type']) ? $args['post_type'][0] : $args['post_type'];
+		$slug = jvbNoBase($postType);
+		$registrar = Registrar::getInstance($slug);
+		if ($registrar && $registrar->hasFeature('is_timeline')) {
+			$args['post_parent'] = 0;
+		}
+		if ($registrar && $registrar->hasFeature('is_content')) {
+			return $this->handleContentTaxonomies($args);
+		}
+		$args['fields'] = 'ids';
+		// Get post IDs
+		$query = new WP_Query($args);
 
-        $page = $args['paged'] ?? 1;
-        $per_page = $args['posts_per_page'] ?? $this->per_page;
-        $has_more = ($page * $per_page) < $total;
+		// Batch prefetch related data
+		update_meta_cache('post', $query->posts);
+		update_object_term_cache($query->posts, $args['post_type']);
 
-        return [
-            'items' => $items,
-            'has_more' => $has_more,
-            'total' => $total
-        ];
-    }
-    /**
-     * Build SQL query components for custom table
-     * @param array $args WP_Query style arguments
-     * @param string $table Table name
-     * @param string $taxonomy Taxonomy type
-     * @return array Query components
-     */
-    protected function buildCustomTableQuery(array $args, string $table, string $taxonomy): array
-    {
-        global $wpdb;
+		// Format regular items
+		$items = array_map(fn($post) => $this->formatItem($post), $query->posts);
 
-        $where_conditions = ['1=1'];
-        $joins = [];
-        $params = [];
+		wp_reset_postdata();
+		return [
+			'items' => $items,
+			'has_more' => $query->max_num_pages > $args['paged'],
+			'total' => $query->found_posts
+		];
+	}
 
-        // Handle search
-        if (!empty($args['s'])) {
-            $search = '%' . $wpdb->esc_like($args['s']) . '%';
-            $where_conditions[] = "(ct.name LIKE %s OR ct.slug LIKE %s)";
-            $params[] = $search;
-            $params[] = $search;
-        }
+	protected function handleContentTaxonomies(array $args): array
+	{
 
-        // Handle context filters (e.g., filtering shops by style through relationships)
-        if (!empty($args['context_filter'])) {
-            $context_conditions = $this->buildContextConditions($args['context_filter'], $joins, $params, $taxonomy);
-            if (!empty($context_conditions)) {
-                $where_conditions[] = $context_conditions;
-            }
-        }
+		$taxonomy = jvbNoBase($args['post_type']);
+		global $wpdb;
+		$table = $wpdb->prefix . BASE . 'content_' . $taxonomy;
 
-        // Handle taxonomy filters (tax_query)
-        if (!empty($args['tax_query'])) {
-            $tax_conditions = $this->buildTaxonomyConditions($args['tax_query'], $joins, $params, $taxonomy);
-            if (!empty($tax_conditions)) {
-                $where_conditions[] = $tax_conditions;
-            }
-        }
+		// Check if table exists
+		if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) {
+			return [
+				'items' => [],
+				'has_more' => false,
+				'total' => 0
+			];
+		}
 
-        // Handle meta queries (custom fields in the table)
-        if (!empty($args['meta_query'])) {
-            $meta_conditions = $this->buildMetaConditions($args['meta_query'], $joins, $params, $taxonomy);
-            if (!empty($meta_conditions)) {
-                $where_conditions[] = $meta_conditions;
-            }
-        }
+		// Build the query components
+		$queryBuilder = $this->buildCustomTableQuery($args, $table, $taxonomy);
 
-        // Handle date queries
-        if (!empty($args['date_query'])) {
-            $date_conditions = $this->buildDateConditions($args['date_query'], $params);
-            if (!empty($date_conditions)) {
-                $where_conditions[] = $date_conditions;
-            }
-        }
+		// Execute count query first
+		$total = (int)$wpdb->get_var($queryBuilder['count_query']);
 
-        // Handle specific IDs
-        if (!empty($args['include'])) {
-            $placeholders = implode(',', array_fill(0, count($args['include']), '%d'));
-            $where_conditions[] = "ct.term_id IN ({$placeholders})";
-            $params = array_merge($params, $args['include']);
-        }
+		// Execute main query if we have results
+		$items = [];
+		if ($total > 0) {
+			$results = $wpdb->get_results($queryBuilder['main_query'], ARRAY_A);
+			$items = array_map(
+				fn($ID) => $this->formatItem($ID['term_id'], $taxonomy),
+				$results
+			);
+		}
 
-        if (!empty($args['exclude'])) {
-            $placeholders = implode(',', array_fill(0, count($args['exclude']), '%d'));
-            $where_conditions[] = "ct.term_id NOT IN ({$placeholders})";
-            $params = array_merge($params, $args['exclude']);
-        }
+		$page = $args['paged'] ?? 1;
+		$per_page = $args['posts_per_page'] ?? $this->per_page;
+		$has_more = ($page * $per_page) < $total;
 
-        // Build ORDER BY
-        $order_by = $this->buildOrderBy($args, $taxonomy);
+		return [
+			'items' => $items,
+			'has_more' => $has_more,
+			'total' => $total
+		];
+	}
 
-        // Build LIMIT
-        $page = $args['paged'] ?? 1;
-        $per_page = $args['posts_per_page'] ?? $this->per_page;
-        $offset = ($page - 1) * $per_page;
+	/**
+	 * Build SQL query components for custom table
+	 * @param array $args WP_Query style arguments
+	 * @param string $table Table name
+	 * @param string $taxonomy Taxonomy type
+	 * @return array Query components
+	 */
+	protected function buildCustomTableQuery(array $args, string $table, string $taxonomy): array
+	{
+		global $wpdb;
 
-        // Combine everything
-        $joins_sql = !empty($joins) ? implode(' ', $joins) : '';
-        $where_sql = implode(' AND ', $where_conditions);
+		$where_conditions = ['1=1'];
+		$joins = [];
+		$params = [];
 
-        $base_query = "FROM {$table} ct
+		// Handle search
+		if (!empty($args['s'])) {
+			$search = '%' . $wpdb->esc_like($args['s']) . '%';
+			$where_conditions[] = "(ct.name LIKE %s OR ct.slug LIKE %s)";
+			$params[] = $search;
+			$params[] = $search;
+		}
+
+		// Handle context filters (e.g., filtering shops by style through relationships)
+		if (!empty($args['context_filter'])) {
+			$context_conditions = $this->buildContextConditions($args['context_filter'], $joins, $params, $taxonomy);
+			if (!empty($context_conditions)) {
+				$where_conditions[] = $context_conditions;
+			}
+		}
+
+		// Handle taxonomy filters (tax_query)
+		if (!empty($args['tax_query'])) {
+			$tax_conditions = $this->buildTaxonomyConditions($args['tax_query'], $joins, $params, $taxonomy);
+			if (!empty($tax_conditions)) {
+				$where_conditions[] = $tax_conditions;
+			}
+		}
+
+		// Handle meta queries (custom fields in the table)
+		if (!empty($args['meta_query'])) {
+			$meta_conditions = $this->buildMetaConditions($args['meta_query'], $joins, $params, $taxonomy);
+			if (!empty($meta_conditions)) {
+				$where_conditions[] = $meta_conditions;
+			}
+		}
+
+		// Handle date queries
+		if (!empty($args['date_query'])) {
+			$date_conditions = $this->buildDateConditions($args['date_query'], $params);
+			if (!empty($date_conditions)) {
+				$where_conditions[] = $date_conditions;
+			}
+		}
+
+		// Handle specific IDs
+		if (!empty($args['include'])) {
+			$placeholders = implode(',', array_fill(0, count($args['include']), '%d'));
+			$where_conditions[] = "ct.term_id IN ({$placeholders})";
+			$params = array_merge($params, $args['include']);
+		}
+
+		if (!empty($args['exclude'])) {
+			$placeholders = implode(',', array_fill(0, count($args['exclude']), '%d'));
+			$where_conditions[] = "ct.term_id NOT IN ({$placeholders})";
+			$params = array_merge($params, $args['exclude']);
+		}
+
+		// Build ORDER BY
+		$order_by = $this->buildOrderBy($args, $taxonomy);
+
+		// Build LIMIT
+		$page = $args['paged'] ?? 1;
+		$per_page = $args['posts_per_page'] ?? $this->per_page;
+		$offset = ($page - 1) * $per_page;
+
+		// Combine everything
+		$joins_sql = !empty($joins) ? implode(' ', $joins) : '';
+		$where_sql = implode(' AND ', $where_conditions);
+
+		$base_query = "FROM {$table} ct
                    LEFT JOIN {$wpdb->terms} t ON ct.term_id = t.term_id
                    LEFT JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                    {$joins_sql}
                    WHERE {$where_sql}";
 
-        $count_query = "SELECT COUNT(DISTINCT ct.term_id) {$base_query}";
+		$count_query = "SELECT COUNT(DISTINCT ct.term_id) {$base_query}";
 
-        $main_query = "SELECT ct.term_id
+		$main_query = "SELECT ct.term_id
                    {$base_query}
                    {$order_by}
                    LIMIT %d OFFSET %d";
 
-        // Add limit parameters
-        $count_params = $params;
-        $main_params = array_merge($params, [$per_page, $offset]);
+		// Add limit parameters
+		$count_params = $params;
+		$main_params = array_merge($params, [$per_page, $offset]);
 
-        return [
-            'main_query' => $wpdb->prepare($main_query, $main_params),
-            'count_query' => $wpdb->prepare($count_query, $count_params)
-        ];
-    }
+		return [
+			'main_query' => $wpdb->prepare($main_query, $main_params),
+			'count_query' => $wpdb->prepare($count_query, $count_params)
+		];
+	}
 
-    /**
-     * Build context-based filter conditions (e.g., shops filtered by style relationships)
-     * @param array $context_filter Context filter data
-     * @param array &$joins Reference to joins array
-     * @param array &$params Reference to params array
-     * @param string $taxonomy Current taxonomy type
-     * @return string SQL condition
-     */
-    protected function buildContextConditions(array $context_filter, array &$joins, array &$params, string $taxonomy): string
-    {
-        global $wpdb;
+	/**
+	 * Build context-based filter conditions (e.g., shops filtered by style relationships)
+	 * @param array $context_filter Context filter data
+	 * @param array &$joins Reference to joins array
+	 * @param array &$params Reference to params array
+	 * @param string $taxonomy Current taxonomy type
+	 * @return string SQL condition
+	 */
+	protected function buildContextConditions(array $context_filter, array &$joins, array &$params, string $taxonomy): string
+	{
+		global $wpdb;
 
-        $context_type = $context_filter['type'] ?? '';
-        $context_id = $context_filter['id'] ?? 0;
+		$context_type = $context_filter['type'] ?? '';
+		$context_id = $context_filter['id'] ?? 0;
 
-        if (empty($context_type) || empty($context_id)) {
-            return '';
-        }
+		if (empty($context_type) || empty($context_id)) {
+			return '';
+		}
 
-        $relationships_table = $wpdb->prefix . BASE . 'taxonomy_relationships';
+		$relationships_table = $wpdb->prefix . BASE . 'taxonomy_relationships';
 
-        switch ($context_type) {
-            case 'style':
-            case 'theme':
-            case 'pstyle':
-                // For shops filtered by style/theme through relationships
-                if ($taxonomy === 'shop') {
-                    $joins[] = "INNER JOIN {$relationships_table} tr ON ct.term_id = tr.term_id";
-                    $where_condition = "tr.related_term_id = %d AND tr.related_taxonomy = %s";
-                    $params[] = $context_id;
-                    $params[] = BASE . $context_type;
-                    return $where_condition;
-                }
-                break;
+		switch ($context_type) {
+			case 'style':
+			case 'theme':
+			case 'pstyle':
+				// For shops filtered by style/theme through relationships
+				if ($taxonomy === 'shop') {
+					$joins[] = "INNER JOIN {$relationships_table} tr ON ct.term_id = tr.term_id";
+					$where_condition = "tr.related_term_id = %d AND tr.related_taxonomy = %s";
+					$params[] = $context_id;
+					$params[] = BASE . $context_type;
+					return $where_condition;
+				}
+				break;
 
-            case 'city':
-                // Filter by city
-                if (isset($this->getCustomTableFields($taxonomy)['city'])) {
-                    $params[] = $context_id;
-                    return "ct.city = %d";
-                }
-                break;
-        }
+			case 'city':
+				// Filter by city
+				if (isset($this->getCustomTableFields($taxonomy)['city'])) {
+					$params[] = $context_id;
+					return "ct.city = %d";
+				}
+				break;
+		}
 
-        return '';
-    }
+		return '';
+	}
 
-    /**
-     * Build taxonomy filter conditions for custom table
-     * @param array $tax_query Tax query array
-     * @param array &$joins Reference to joins array
-     * @param array &$params Reference to params array
-     * @param string $taxonomy Current taxonomy type
-     * @return string SQL condition
-     */
-    protected function buildTaxonomyConditions(array $tax_query, array &$joins, array &$params, string $taxonomy): string
-    {
-        global $wpdb;
+	/**
+	 * Build taxonomy filter conditions for custom table
+	 * @param array $tax_query Tax query array
+	 * @param array &$joins Reference to joins array
+	 * @param array &$params Reference to params array
+	 * @param string $taxonomy Current taxonomy type
+	 * @return string SQL condition
+	 */
+	protected function buildTaxonomyConditions(array $tax_query, array &$joins, array &$params, string $taxonomy): string
+	{
+		global $wpdb;
 
-        $conditions = [];
-        $relation = $tax_query['relation'] ?? 'AND';
+		$conditions = [];
+		$relation = $tax_query['relation'] ?? 'AND';
 
-        foreach ($tax_query as $key => $query) {
-            if ($key === 'relation' || !is_array($query)) {
-                continue;
-            }
+		foreach ($tax_query as $key => $query) {
+			if ($key === 'relation' || !is_array($query)) {
+				continue;
+			}
 
-            $query_taxonomy = $query['taxonomy'] ?? '';
-            $terms = (array)($query['terms'] ?? []);
-            $field = $query['field'] ?? 'term_id';
-            $operator = $query['operator'] ?? 'IN';
+			$query_taxonomy = $query['taxonomy'] ?? '';
+			$terms = (array)($query['terms'] ?? []);
+			$field = $query['field'] ?? 'term_id';
+			$operator = $query['operator'] ?? 'IN';
 
-            if (empty($query_taxonomy) || empty($terms)) {
-                continue;
-            }
+			if (empty($query_taxonomy) || empty($terms)) {
+				continue;
+			}
 
-            // Check if this taxonomy field exists in our custom table
-            $custom_fields = $this->getCustomTableFields($taxonomy);
-            $taxonomy_clean = str_replace(BASE, '', $query_taxonomy);
+			// Check if this taxonomy field exists in our custom table
+			$custom_fields = $this->getCustomTableFields($taxonomy);
+			$taxonomy_clean = str_replace(BASE, '', $query_taxonomy);
 
-            if (isset($custom_fields[$taxonomy_clean])) {
-                // Field exists in custom table - direct query
-                $field_column = "ct.{$taxonomy_clean}";
+			if (isset($custom_fields[$taxonomy_clean])) {
+				// Field exists in custom table - direct query
+				$field_column = "ct.{$taxonomy_clean}";
 
-                if ($field === 'slug') {
-                    // Need to convert slugs to IDs first
-                    $term_ids = [];
-                    foreach ($terms as $slug) {
-                        $term = get_term_by('slug', $slug, $query_taxonomy);
-                        if ($term) {
-                            $term_ids[] = $term->term_id;
-                        }
-                    }
-                    $terms = $term_ids;
-                }
+				if ($field === 'slug') {
+					// Need to convert slugs to IDs first
+					$term_ids = [];
+					foreach ($terms as $slug) {
+						$term = get_term_by('slug', $slug, $query_taxonomy);
+						if ($term) {
+							$term_ids[] = $term->term_id;
+						}
+					}
+					$terms = $term_ids;
+				}
 
-                if (!empty($terms)) {
-                    $placeholders = implode(',', array_fill(0, count($terms), '%d'));
-                    $conditions[] = "{$field_column} {$operator} ({$placeholders})";
-                    $params = array_merge($params, $terms);
-                }
-            } else {
-                // Need to join with term relationships
-                $join_alias = "tr_{$key}";
-                $joins[] = "LEFT JOIN {$wpdb->term_relationships} {$join_alias} ON ct.term_id = {$join_alias}.object_id";
-                $joins[] = "LEFT JOIN {$wpdb->term_taxonomy} tt_{$key} ON {$join_alias}.term_taxonomy_id = tt_{$key}.term_taxonomy_id";
+				if (!empty($terms)) {
+					$placeholders = implode(',', array_fill(0, count($terms), '%d'));
+					$conditions[] = "{$field_column} {$operator} ({$placeholders})";
+					$params = array_merge($params, $terms);
+				}
+			} else {
+				// Need to join with term relationships
+				$join_alias = "tr_{$key}";
+				$joins[] = "LEFT JOIN {$wpdb->term_relationships} {$join_alias} ON ct.term_id = {$join_alias}.object_id";
+				$joins[] = "LEFT JOIN {$wpdb->term_taxonomy} tt_{$key} ON {$join_alias}.term_taxonomy_id = tt_{$key}.term_taxonomy_id";
 
-                if ($field === 'slug') {
-                    $joins[] = "LEFT JOIN {$wpdb->terms} t_{$key} ON tt_{$key}.term_id = t_{$key}.term_id";
-                    $field_column = "t_{$key}.slug";
-                    $term_values = $terms; // Use slugs directly
-                } else {
-                    $field_column = "tt_{$key}.term_id";
-                    $term_values = array_map('intval', $terms);
-                }
+				if ($field === 'slug') {
+					$joins[] = "LEFT JOIN {$wpdb->terms} t_{$key} ON tt_{$key}.term_id = t_{$key}.term_id";
+					$field_column = "t_{$key}.slug";
+					$term_values = $terms; // Use slugs directly
+				} else {
+					$field_column = "tt_{$key}.term_id";
+					$term_values = array_map('intval', $terms);
+				}
 
-                $placeholders = implode(',', array_fill(0, count($term_values), $field === 'slug' ? '%s' : '%d'));
-                $taxonomy_condition = "tt_{$key}.taxonomy = %s";
-                $terms_condition = "{$field_column} {$operator} ({$placeholders})";
+				$placeholders = implode(',', array_fill(0, count($term_values), $field === 'slug' ? '%s' : '%d'));
+				$taxonomy_condition = "tt_{$key}.taxonomy = %s";
+				$terms_condition = "{$field_column} {$operator} ({$placeholders})";
 
-                $conditions[] = "({$taxonomy_condition} AND {$terms_condition})";
-                $params[] = $query_taxonomy;
-                $params = array_merge($params, $term_values);
-            }
-        }
+				$conditions[] = "({$taxonomy_condition} AND {$terms_condition})";
+				$params[] = $query_taxonomy;
+				$params = array_merge($params, $term_values);
+			}
+		}
 
-        return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
-    }
+		return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
+	}
 
-    /**
-     * Build meta query conditions for custom table fields
-     * @param array $meta_query Meta query array
-     * @param array &$joins Reference to joins array
-     * @param array &$params Reference to params array
-     * @param string $taxonomy Taxonomy type
-     * @return string SQL condition
-     */
-    protected function buildMetaConditions(array $meta_query, array &$joins, array &$params, string $taxonomy): string
-    {
-        global $wpdb;
-        $conditions = [];
-        $relation = $meta_query['relation'] ?? 'AND';
+	/**
+	 * Build meta query conditions for custom table fields
+	 * @param array $meta_query Meta query array
+	 * @param array &$joins Reference to joins array
+	 * @param array &$params Reference to params array
+	 * @param string $taxonomy Taxonomy type
+	 * @return string SQL condition
+	 */
+	protected function buildMetaConditions(array $meta_query, array &$joins, array &$params, string $taxonomy): string
+	{
+		global $wpdb;
+		$conditions = [];
+		$relation = $meta_query['relation'] ?? 'AND';
 
-        // Get fields for this taxonomy to know which are in the custom table
-        $custom_fields = $this->getCustomTableFields($taxonomy);
+		// Get fields for this taxonomy to know which are in the custom table
+		$custom_fields = $this->getCustomTableFields($taxonomy);
 
-        foreach ($meta_query as $key => $query) {
-            if ($key === 'relation' || !is_array($query)) {
-                continue;
-            }
+		foreach ($meta_query as $key => $query) {
+			if ($key === 'relation' || !is_array($query)) {
+				continue;
+			}
 
-            $meta_key = $query['key'] ?? '';
-            $meta_value = $query['value'] ?? '';
-            $compare = $query['compare'] ?? '=';
+			$meta_key = $query['key'] ?? '';
+			$meta_value = $query['value'] ?? '';
+			$compare = $query['compare'] ?? '=';
 
-            if (empty($meta_key)) {
-                continue;
-            }
+			if (empty($meta_key)) {
+				continue;
+			}
 
-            // Remove BASE prefix if present
-            $clean_key = str_replace(BASE, '', $meta_key);
+			// Remove BASE prefix if present
+			$clean_key = str_replace(BASE, '', $meta_key);
 
-            // Check if this field exists in our custom table
-            if (isset($custom_fields[$clean_key])) {
-                // Field is in custom table, query directly
-                $column = "ct.{$clean_key}";
-                $condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
-                if ($condition) {
-                    $conditions[] = $condition;
-                }
-            } else {
-                // Field is in term meta, need to join
-                $join_alias = "tm_{$key}";
-                $joins[] = "LEFT JOIN {$wpdb->termmeta} {$join_alias} ON ct.term_id = {$join_alias}.term_id AND {$join_alias}.meta_key = %s";
-                $params[] = $meta_key;
+			// Check if this field exists in our custom table
+			if (isset($custom_fields[$clean_key])) {
+				// Field is in custom table, query directly
+				$column = "ct.{$clean_key}";
+				$condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
+				if ($condition) {
+					$conditions[] = $condition;
+				}
+			} else {
+				// Field is in term meta, need to join
+				$join_alias = "tm_{$key}";
+				$joins[] = "LEFT JOIN {$wpdb->termmeta} {$join_alias} ON ct.term_id = {$join_alias}.term_id AND {$join_alias}.meta_key = %s";
+				$params[] = $meta_key;
 
-                $column = "{$join_alias}.meta_value";
-                $condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
-                if ($condition) {
-                    $conditions[] = $condition;
-                }
-            }
-        }
+				$column = "{$join_alias}.meta_value";
+				$condition = $this->buildMetaComparison($column, $meta_value, $compare, $params);
+				if ($condition) {
+					$conditions[] = $condition;
+				}
+			}
+		}
 
-        return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
-    }
+		return !empty($conditions) ? '(' . implode(" {$relation} ", $conditions) . ')' : '';
+	}
 
-    /**
-     * Build comparison condition for meta fields
-     * @param string $column Column name
-     * @param mixed $value Value to compare
-     * @param string $compare Comparison operator
-     * @param array &$params Reference to params array
-     * @return string SQL condition
-     */
-    protected function buildMetaComparison(string $column, $value, string $compare, array &$params): string
-    {
-        switch (strtoupper($compare)) {
-            case 'LIKE':
-                $params[] = '%' . $value . '%';
-                return "{$column} LIKE %s";
+	/**
+	 * Build comparison condition for meta fields
+	 * @param string $column Column name
+	 * @param mixed $value Value to compare
+	 * @param string $compare Comparison operator
+	 * @param array &$params Reference to params array
+	 * @return string SQL condition
+	 */
+	protected function buildMetaComparison(string $column, $value, string $compare, array &$params): string
+	{
+		switch (strtoupper($compare)) {
+			case 'LIKE':
+				$params[] = '%' . $value . '%';
+				return "{$column} LIKE %s";
 
-            case 'NOT LIKE':
-                $params[] = '%' . $value . '%';
-                return "{$column} NOT LIKE %s";
+			case 'NOT LIKE':
+				$params[] = '%' . $value . '%';
+				return "{$column} NOT LIKE %s";
 
-            case 'IN':
-                if (is_array($value)) {
-                    $placeholders = implode(',', array_fill(0, count($value), '%s'));
-                    $params = array_merge($params, $value);
-                    return "{$column} IN ({$placeholders})";
-                }
-                break;
+			case 'IN':
+				if (is_array($value)) {
+					$placeholders = implode(',', array_fill(0, count($value), '%s'));
+					$params = array_merge($params, $value);
+					return "{$column} IN ({$placeholders})";
+				}
+				break;
 
-            case 'NOT IN':
-                if (is_array($value)) {
-                    $placeholders = implode(',', array_fill(0, count($value), '%s'));
-                    $params = array_merge($params, $value);
-                    return "{$column} NOT IN ({$placeholders})";
-                }
-                break;
+			case 'NOT IN':
+				if (is_array($value)) {
+					$placeholders = implode(',', array_fill(0, count($value), '%s'));
+					$params = array_merge($params, $value);
+					return "{$column} NOT IN ({$placeholders})";
+				}
+				break;
 
-            case 'BETWEEN':
-                if (is_array($value) && count($value) === 2) {
-                    $params[] = $value[0];
-                    $params[] = $value[1];
-                    return "{$column} BETWEEN %s AND %s";
-                }
-                break;
+			case 'BETWEEN':
+				if (is_array($value) && count($value) === 2) {
+					$params[] = $value[0];
+					$params[] = $value[1];
+					return "{$column} BETWEEN %s AND %s";
+				}
+				break;
 
-            case '!=':
-            case '<>':
-                $params[] = $value;
-                return "{$column} != %s";
+			case '!=':
+			case '<>':
+				$params[] = $value;
+				return "{$column} != %s";
 
-            case '>':
-                $params[] = $value;
-                return "{$column} > %s";
+			case '>':
+				$params[] = $value;
+				return "{$column} > %s";
 
-            case '>=':
-                $params[] = $value;
-                return "{$column} >= %s";
+			case '>=':
+				$params[] = $value;
+				return "{$column} >= %s";
 
-            case '<':
-                $params[] = $value;
-                return "{$column} < %s";
+			case '<':
+				$params[] = $value;
+				return "{$column} < %s";
 
-            case '<=':
-                $params[] = $value;
-                return "{$column} <= %s";
+			case '<=':
+				$params[] = $value;
+				return "{$column} <= %s";
 
-            case '=':
-            default:
-                $params[] = $value;
-                return "{$column} = %s";
-        }
+			case '=':
+			default:
+				$params[] = $value;
+				return "{$column} = %s";
+		}
 
-        return '';
-    }
+		return '';
+	}
 
-    /**
-     * Build date query conditions
-     * @param array $date_query Date query array
-     * @param array &$params Reference to params array
-     * @return string SQL condition
-     */
-    protected function buildDateConditions(array $date_query, array &$params): string
-    {
-        $conditions = [];
+	/**
+	 * Build date query conditions
+	 * @param array $date_query Date query array
+	 * @param array &$params Reference to params array
+	 * @return string SQL condition
+	 */
+	protected function buildDateConditions(array $date_query, array &$params): string
+	{
+		$conditions = [];
 
-        foreach ($date_query as $query) {
-            if (!is_array($query)) continue;
+		foreach ($date_query as $query) {
+			if (!is_array($query)) continue;
 
-            $column = $query['column'] ?? 'updated_at';
-            $year = $query['year'] ?? null;
-            $month = $query['month'] ?? null;
-            $day = $query['day'] ?? null;
-            $after = $query['after'] ?? null;
-            $before = $query['before'] ?? null;
+			$column = $query['column'] ?? 'updated_at';
+			$year = $query['year'] ?? null;
+			$month = $query['month'] ?? null;
+			$day = $query['day'] ?? null;
+			$after = $query['after'] ?? null;
+			$before = $query['before'] ?? null;
 
-            if ($year) {
-                $params[] = $year;
-                $conditions[] = "YEAR(ct.{$column}) = %d";
-            }
+			if ($year) {
+				$params[] = $year;
+				$conditions[] = "YEAR(ct.{$column}) = %d";
+			}
 
-            if ($month) {
-                $params[] = $month;
-                $conditions[] = "MONTH(ct.{$column}) = %d";
-            }
+			if ($month) {
+				$params[] = $month;
+				$conditions[] = "MONTH(ct.{$column}) = %d";
+			}
 
-            if ($day) {
-                $params[] = $day;
-                $conditions[] = "DAY(ct.{$column}) = %d";
-            }
+			if ($day) {
+				$params[] = $day;
+				$conditions[] = "DAY(ct.{$column}) = %d";
+			}
 
-            if ($after) {
-                $params[] = $after;
-                $conditions[] = "ct.{$column} > %s";
-            }
+			if ($after) {
+				$params[] = $after;
+				$conditions[] = "ct.{$column} > %s";
+			}
 
-            if ($before) {
-                $params[] = $before;
-                $conditions[] = "ct.{$column} < %s";
-            }
-        }
+			if ($before) {
+				$params[] = $before;
+				$conditions[] = "ct.{$column} < %s";
+			}
+		}
 
-        return !empty($conditions) ? '(' . implode(' AND ', $conditions) . ')' : '';
-    }
+		return !empty($conditions) ? '(' . implode(' AND ', $conditions) . ')' : '';
+	}
 
-    /**
-     * Build ORDER BY clause
-     * @param array $args Query arguments
-     * @param string $taxonomy Taxonomy type
-     * @return string ORDER BY clause
-     */
-    protected function buildOrderBy(array $args, string $taxonomy): string
-    {
-        $orderby = $args['orderby'] ?? 'name';
-        $order = $args['order'] ?? 'ASC';
+	/**
+	 * Build ORDER BY clause
+	 * @param array $args Query arguments
+	 * @param string $taxonomy Taxonomy type
+	 * @return string ORDER BY clause
+	 */
+	protected function buildOrderBy(array $args, string $taxonomy): string
+	{
+		$orderby = $args['orderby'] ?? 'name';
+		$order = $args['order'] ?? 'ASC';
 
-        // Validate order direction
-        if (!in_array($order, ['ASC', 'DESC'])) {
-            $order = 'ASC';
-        }
+		// Validate order direction
+		if (!in_array($order, ['ASC', 'DESC'])) {
+			$order = 'ASC';
+		}
 
-        if (str_contains($orderby, 'RAND')) {
-            return "ORDER BY {$orderby}";
-        }
+		if (str_contains($orderby, 'RAND')) {
+			return "ORDER BY {$orderby}";
+		}
 
-        switch ($orderby) {
-            case 'name':
-                return "ORDER BY ct.name {$order}";
+		switch ($orderby) {
+			case 'name':
+				return "ORDER BY ct.name {$order}";
 
-            case 'count':
-                return "ORDER BY tt.count {$order}";
+			case 'count':
+				return "ORDER BY tt.count {$order}";
 
-            case 'term_id':
-            case 'id':
-                return "ORDER BY ct.term_id {$order}";
+			case 'term_id':
+			case 'id':
+				return "ORDER BY ct.term_id {$order}";
 
-            case 'slug':
-                return "ORDER BY t.slug {$order}";
+			case 'slug':
+				return "ORDER BY t.slug {$order}";
 
-            case 'date':
-            case 'updated':
-                return "ORDER BY ct.updated_at {$order}";
+			case 'date':
+			case 'updated':
+				return "ORDER BY ct.updated_at {$order}";
 
-            default:
-                // Check if it's a custom field in our table
-                $custom_fields = $this->getCustomTableFields($taxonomy);
-                if (isset($custom_fields[$orderby])) {
-                    return "ORDER BY ct.{$orderby} {$order}";
-                }
+			default:
+				// Check if it's a custom field in our table
+				$custom_fields = $this->getCustomTableFields($taxonomy);
+				if (isset($custom_fields[$orderby])) {
+					return "ORDER BY ct.{$orderby} {$order}";
+				}
 
-                // Default to name
-                return "ORDER BY ct.name {$order}";
-        }
-    }
+				// Default to name
+				return "ORDER BY ct.name {$order}";
+		}
+	}
 
-    /**
-     * 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)
+	 * Returns structured data about content types that can be shown in feed
+	 */
+	public function getFeedTypes(WP_REST_Request $request): WP_REST_Response
+	{
+		// Check HTTP cache
+		$cache_check = $this->checkHeaders($request, 'feed_types');
+		if ($cache_check) {
+			return $cache_check;
+		}
+
+		$feedTypes = $this->buildFeedTypesConfig();
+
+		$response = $this->success($feedTypes);
+		return $this->addCacheHeaders($response);
+	}
+
+	public function getFeedTypesConfig():array
+	{
+		return $this->buildFeedTypesConfig();
+	}
+	/**
+	 * Build feed types configuration from Features
+	 */
+	protected function buildFeedTypesConfig(): array
+	{
+		return $this->cache->remember(
+			'contentTypes',
+			function () {
+				$config = [];
+
+				// Get content types with show_feed
+				$contentTypes = Registrar::getFeatured('show_feed', 'post');
+				foreach ($contentTypes as $slug) {
+					$this->cache->tag('content:'.$slug);
+					$registrar = Registrar::getInstance($slug);
+					if (!$registrar) continue;
+
+					$config[$slug] = [
+						'type' => 'content',
+						'singular' => $registrar->getSingular(),
+						'plural' => $registrar->getPlural(),
+						'icon' => $registrar->getIcon(),
+						'taxonomies' => $registrar->registrar->taxonomies,
+					];
+				}
+
+				// Get taxonomies with show_feed (content taxonomies)
+				$taxonomies = Registrar::getFeatured('show_feed', 'term');
+				foreach ($taxonomies as $slug) {
+					$registrar = Registrar::getInstance($slug);
+					if (!$registrar || !($registrar->hasFeature('is_content') ?? false)) {
+						continue;
+					}
+
+					$this->cache->tag('taxonomy:'.$slug);
+
+					$config[$slug] = [
+						'type' => 'taxonomy',
+						'singular' => $registrar->getSingular(),
+						'plural' => $registrar->getPlural(),
+						'icon' => $registrar->getIcon(),
+						'taxonomies' => [], // Content taxonomies don't have sub-taxonomies
+						'for_content' => $registrar->registrar->for ?? []
+					];
+				}
+
+				return $config;
+			});
+	}
 }

--
Gitblit v1.10.0