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 |  538 ++++++++++++++++++++++++-----------------------------------
 1 files changed, 221 insertions(+), 317 deletions(-)

diff --git a/inc/rest/routes/FeedRoutes.php b/inc/rest/routes/FeedRoutes.php
index 05a97a7..4efc099 100644
--- a/inc/rest/routes/FeedRoutes.php
+++ b/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->clear();
+		$this->cache
+			->connect('post', true)
+			->connect('taxonomy', true)
+			->connect('user', true);
+
+		if (JVB_TESTING) {
+			$this->cache->flush();
+		}
+
 	}
 
 	public function init():void
 	{
-		$this->cache->clear();
-		$this->checker = Checker::getInstance();
-
-		if (Features::hasIntegration('umami')) {
+		if (Site::hasIntegration('umami')) {
 			$this->tracker = JVB()->connect('umami');
 		}
-		$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,
-//			function() use ($postID, $type, $metaType, $post, $skip) {
-				$config = null;
+
+		return $this->cache->remember(
+			$postID,
+			function() use ($postID, $type, $metaType, $post, $skip) {
+				$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 = [
@@ -166,7 +189,10 @@
 
 				$out['id'] = $postID;
 				$out['content'] = $type;
-				$out['icon'] = $config['icon']??jvbDefaultIcon();
+				$out['icon'] = $registrar->getIcon()??jvbDefaultIcon();
+				if ($out['icon'] === '') {
+					$out['icon'] = jvbDefaultIcon();
+				}
 
 				if ($this->tracker) {
 					$args = ($metaType === 'post') ? ['owner_id' => $post->post_author] : [];
@@ -177,34 +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;
-//			}
-//		);
-		return $out;
+				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){
@@ -230,7 +258,7 @@
 		}
 		$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
@@ -242,20 +270,21 @@
 		$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']['number'] = count($children);
+		$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 {
@@ -264,14 +293,12 @@
 			if (empty($value)) {
 				continue;
 			}
-			if (!array_key_exists($key, JVB_TAXONOMY)) {
+
+			$registrar = Registrar::getInstance($key);
+			if (!$registrar || $registrar->registrar->public === false){
 				continue;
 			}
 
-			$taxConfig = JVB_TAXONOMY[$key];
-			if (isset($taxConfig['public']) && $taxConfig['public'] === false) {
-				continue;
-			}
 			$terms = array_map('absint', explode(',', $value));
 			$terms = array_filter($terms); // Remove 0 values
 
@@ -290,13 +317,12 @@
 
 	protected function formatTaxonomy(WP_Term|int $term, int $postID, string $type)
 	{
-		$cache = CacheManager::for(jvbNoBase($term->taxonomy));
-		return $cache->remember(
-			'feed_link_'.$term->term_id,
+		return $this->cache->remember(
+			$term->term_id,
 			function () use ($term, $postID, $type) {
 				$base = [
 					'ID' => $term->term_id,
-					'title' => htmlspecialchars_decode($term->name),
+					'title' => html_entity_decode($term->name),
 					'url' => get_term_link($term->term_id, $term->taxonomy),
 				];
 				if ($this->tracker) {
@@ -311,43 +337,60 @@
 
 	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;
@@ -359,8 +402,7 @@
 		$data = $request->get_params();
 		$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,
 		];
@@ -384,44 +426,6 @@
 		return $this->applyFavouritesFilter($args, $data);
 	}
 
-	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;
-	}
-
 	/**
 	 * @param WP_REST_Request $request
 	 *
@@ -430,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
 		}
 
-		$key = $this->cache->generateKey($args);
 		$cached = $this->cache->get($key);
 		if ($cached) {
 			if ($request->get_param('highlight')) {
@@ -450,13 +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);
 
-		$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')) {
@@ -465,86 +467,12 @@
 		}
 
 		$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
@@ -563,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;
 	}
@@ -588,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;
 	}
 
@@ -636,38 +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;
 		}
-		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'));
-		$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;
 	}
@@ -681,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';
@@ -1212,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)
@@ -1229,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);
 	}
 
@@ -1249,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 ?? []
 					];
 				}
 

--
Gitblit v1.10.0