From 3b3bd067d0ff2671fca2890c14428c97e1011a2b Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 11 Feb 2026 03:20:21 +0000
Subject: [PATCH] =Hey guess what? Meta->set() and Meta->setAll() doesn't actually save the meta to the database. Went through the files and added a ->save() call after every set

---
 /dev/null                                            | 1234 -------------------------------------
 inc/helpers/time.php                                 |    1 
 inc/managers/queue/executors/UploadExecutor.php      |    8 
 inc/integrations/Square.php                          |    3 
 inc/managers/queue/executors/ContentExecutor.php     |    3 
 inc/rest/routes/UploadRoutes.php                     |  688 --------------------
 inc/rest/routes/SettingsRoutes.php                   |    2 
 inc/rest/routes/ShopRoutes.php                       |    7 
 inc/managers/queue/executors/ContentTermExecutor.php |    6 
 inc/integrations/GoogleMyBusiness.php                |    4 
 inc/rest/routes/NewsRoutes.php                       |    1 
 11 files changed, 29 insertions(+), 1,928 deletions(-)

diff --git a/inc/helpers/time.php b/inc/helpers/time.php
index 241dc1c..e83d48b 100644
--- a/inc/helpers/time.php
+++ b/inc/helpers/time.php
@@ -395,6 +395,7 @@
  * @param array $hours_data Day-based hours data
  * @param string $timezone Timezone string
  * @return string|null Next opening time description or null if never opens
+ * @throws DateInvalidTimeZoneException
  */
 function jvbGetNextOpeningTime(array $hours_data, string $timezone = 'America/Edmonton'): ?string {
 	if (!jvbHasOperatingHours($hours_data)) {
diff --git a/inc/integrations/GoogleMyBusiness.php b/inc/integrations/GoogleMyBusiness.php
index 9e86377..c86d3ab 100644
--- a/inc/integrations/GoogleMyBusiness.php
+++ b/inc/integrations/GoogleMyBusiness.php
@@ -319,6 +319,8 @@
 		} else {
 			$result = $this->createPost($data);
 			$meta->set("_{$this->service_name}_item_id", $result['name']);
+
+			$meta->save();
 		}
 
 		return [
@@ -372,6 +374,7 @@
 		} else {
 			$result = $this->createPost($data);
 			$meta->set("_{$this->service_name}_item_id", $result['name']);
+			$meta->save();
 		}
 
 		return [
@@ -423,6 +426,7 @@
 		} else {
 			$result = $this->createPost($data);
 			$meta->set("_{$this->service_name}_item_id", $result['name']);
+			$meta->save();
 		}
 
 		return [
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index 8c0f234..d889bfa 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -1887,6 +1887,7 @@
 			}
 
 			$meta->setAll($updates);
+			$meta->save();
 
 			// Trigger notification to customer if order is ready
 			if ($state === 'PREPARED') {
@@ -2338,6 +2339,7 @@
 
 		// Save all values at once
 		$meta->setAll($values_to_save);
+		$meta->save();
 	}
 
 	/**
@@ -3462,6 +3464,7 @@
 			'created_at' => current_time('mysql'),
 			'updated_at' => current_time('mysql')
 		]);
+		$meta->save();
 
 		// Index by Square order ID for quick webhook lookups
 		update_option(BASE . 'square_order_map_' . $order_data['square_order_id'], $order_post_id);
diff --git a/inc/managers/SEOMetaManager.php b/inc/managers/SEOMetaManager.php
deleted file mode 100644
index dc9c2de..0000000
--- a/inc/managers/SEOMetaManager.php
+++ /dev/null
@@ -1,832 +0,0 @@
-<?php
-namespace JVBase\managers;
-
-use JVBase\meta\Meta;
-use WP_Term;
-use DateTime;
-use DateMalformedStringException;
-
-if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
-}
-/**
- * @deprecated use JVBase\managers\seo\SEO.php
- * SEO Meta Manager for edmonton.ink
- *
- * Integrates with The SEO Framework to generate optimized titles and meta descriptions
- * for various content types in the edmonton.ink site.
- */
-class SEOMetaManager
-{
-    protected array $content_types;
-    protected array $taxonomies;
-    protected array $character_limits = [
-        'title' => 60, // Recommended character limit for titles
-        'description' => 155, // Recommended character limit for descriptions
-    ];
-
-    /**
-     * Initialize the SEO Meta Manager
-     */
-    public function __construct()
-    {
-        // Hook into The SEO Framework filters for titles and descriptions
-        add_filter('the_seo_framework_title_from_generation', [ $this, 'customizeTitle' ], 10, 2);
-        add_filter('the_seo_framework_generated_description', [ $this, 'customDescription' ], 10, 3);
-
-        // Initialize content types and taxonomies on init hook
-        add_action('init', [ $this, 'init' ]);
-    }
-
-    /**
-     * Initialize content types and taxonomies on WordPress init
-     * @return void
-     */
-    public function init()
-    {
-        // Filter post types
-        $this->content_types = array_keys(JVB_CONTENT);
-        // Filter term taxonomies
-        $this->taxonomies = array_keys(JVB_TAXONOMY);
-    }
-
-    /**
-     * Customize the title based on content type
-     *
-     * @param string $title The current title
-     * @param array|null $args The title arguments
-     * @return string The customized title
-     */
-    public function customizeTitle(string $title, array|null $args):string
-    {
-        $tsf = tsf();
-        if (null === $args) {
-            // We're in the loop.
-            if ($tsf->query()->is_singular() && in_array($tsf->query()->get_current_post_type(), $this->content_types)) {
-                return $this->getPostTitle($tsf->query()->get_the_real_id(true));
-            } elseif ($tsf->query()->is_tax() && in_array( $tsf->query()->get_current_taxonomy(), $this->taxonomies)) {
-                $term = get_term($tsf->query()->get_the_real_id(true));
-                if ($term && !is_wp_error($term)) {
-                    return $this->getTermTitle($term);
-                }
-            }
-        } else {
-            //we're on the admin page or generating breadcrumb
-        }
-
-        return $title;
-    }
-
-    /**
-     * Customize the description based on content type
-     *
-     * @param string $description The current description
-     * @param array|null $args The description arguments
-     * @param string $type The post/term ID
-     * @return string The customized description
-     */
-    public function customDescription(string $description, array|null $args, string $type):string
-    {
-        $tsf = tsf();
-        if (null == $args) {
-            //We're in the loop
-            if ($tsf->query()->is_singular() && in_array($tsf->query()->get_current_post_type(), $this->content_types)) {
-                return $this->getPostDescription($tsf->query()->get_the_real_id(true));
-            } elseif ($tsf->query()->is_tax() && in_array($tsf->query()->get_current_taxonomy(), $this->taxonomies)) {
-                $term = get_term($tsf->query()->get_the_real_id(true));
-                if ($term && !is_wp_error($term)) {
-                    return $this->getTermDescription($term);
-                }
-            }
-        } else {
-            //We're on the admin page or generating...
-        }
-
-        return $description;
-    }
-
-    /**
-     * Get optimized title for a post
-     *
-     * @param int $post_id The post ID
-     * @return string The optimized title
-     */
-    protected function getPostTitle(int $post_id):string
-    {
-        $title = get_the_title($post_id);
-        $meta = Meta::forPost($post_id);
-        $post_type = get_post_type($post_id);
-
-        return match ($post_type) {
-            BASE . 'artist' => $this->getArtistTitle($title, $meta),
-            BASE . 'partner' => $this->getPartnerTitle($title),
-            BASE . 'event' => $this->getEventTitle($post_id, $title),
-            default => $title,
-        };
-    }
-
-    /**
-     * Get optimized description for a post
-     *
-     * @param int $post_id The post ID
-     * @return string The optimized description
-     */
-    protected function getPostDescription(int $post_id):string
-    {
-        $meta = Meta::forPost($post_id);
-
-        $post_type = get_post_type($post_id);
-        switch ($post_type) {
-            case BASE . 'artist':
-                return $this->getArtistDescription($post_id, $meta);
-
-            case BASE . 'partner':
-                return $this->getPartnerDescription($post_id, $meta);
-
-            case BASE . 'event':
-                return $this->getEventDescription($post_id);
-
-            default:
-                return get_the_excerpt($post_id);
-        }
-    }
-
-    /**
-     * Get optimized title for a term
-     *
-     * @param WP_Term $term The term object
-     * @return string The optimized title
-     */
-    protected function getTermTitle(WP_Term $term):string
-    {
-        $meta = Meta::forTerm($term->term_id);
-
-        switch ($term->taxonomy) {
-            case BASE . 'shop':
-                return $this->getShopTitle($term, $meta);
-
-            case BASE . 'style':
-                return $this->getStyleTitle($term);
-
-            case BASE . 'theme':
-                return $this->getThemeTitle($term);
-
-            case BASE . 'city':
-                return $this->getCityTitle($term);
-
-            default:
-                return html_entity_decode($term->name);
-        }
-    }
-
-    /**
-     * Get optimized description for a term
-     *
-     * @param WP_Term $term The term object
-     * @return string The optimized description
-     */
-    protected function getTermDescription(WP_Term $term):string
-    {
-        $meta = Meta::forTerm($term->term_id);
-
-        switch ($term->taxonomy) {
-            case BASE . 'shop':
-                return $this->getShopDescription($term, $meta);
-
-            case BASE . 'style':
-                return $this->getStyleDescription($term, $meta);
-
-            case BASE . 'theme':
-                return $this->getThemeDescription($term, $meta);
-
-            case BASE . 'city':
-                return $this->getCityDescription($term);
-
-            default:
-                return wp_strip_all_tags($term->description);
-        }
-    }
-
-    /**
-     * Get title for archive pages
-     *
-     * @param string $post_type The post type
-     * @return string The archive title
-     */
-    protected function getArchiveTitle(string $post_type):string
-    {
-        return match ($post_type) {
-            BASE . 'artist' => 'Edmonton\'s Best Tattoo Artists & Piercers | edmonton.ink',
-            BASE . 'partner' => 'Our Community Partners | edmonton.ink',
-            BASE . 'tattoo' => 'Edmonton\'s Best Tattoos | edmonton.ink',
-            BASE . 'piercing' => 'Edmonton\'s Best Piercings | edmonton.ink',
-            BASE . 'artwork' => 'Edmonton\'s Best Artwork | edmonton.ink',
-            BASE . 'event' => 'Edmonton Tattoo Events | edmonton.ink',
-            default => post_type_archive_title('', false),
-        };
-    }
-
-    /**
-     * Get description for archive pages
-     *
-     * @param string $post_type The post type
-     * @return string The archive description
-     */
-    protected function getArchiveDescription(string $post_type):string
-    {
-        return match ($post_type) {
-            BASE . 'artist' => 'Explore Edmonton\'s best tattoo artists and piercers in our comprehensive directory. Find artists by style, specialty, or location.',
-            BASE . 'partner' => 'Meet the businesses and organizations supporting Edmonton\'s tattoo and body art community. Our partners help make edmonton.ink possible.',
-            BASE . 'tattoo' => 'Browse a collection of tattoos from Edmonton\'s talented artists. Filter by style, theme, or artist to find your inspiration.',
-            BASE . 'piercing' => 'Discover piercings by Edmonton\'s skilled professionals. Browse our gallery to find your next piercing inspiration.',
-            BASE . 'artwork' => 'Explore original artwork by Edmonton\'s tattoo artists. See custom designs, prints, and paintings from local talent.',
-            BASE . 'event' => 'Stay up-to-date with Edmonton\'s tattoo and piercing events. Find flash days, guest artists, conventions, and more.',
-            default => '',
-        };
-    }
-
-    /**
-     * Truncate text to a specific length while respecting word boundaries
-     *
-     * @param string $text The text to truncate
-     * @param int $limit The character limit
-     * @param string $suffix The suffix to add if truncated (default: '...')
-     * @return string The truncated text
-     */
-    protected function truncate(string $text, int $limit, string $suffix = '...'):string
-    {
-        $text = wp_strip_all_tags($text);
-
-        if (mb_strlen($text) <= $limit) {
-            return $text;
-        }
-
-        // Find the last space within the limit
-        $truncated = mb_substr($text, 0, $limit);
-        $last_space = mb_strrpos($truncated, ' ');
-
-        if ($last_space !== false) {
-            $truncated = mb_substr($truncated, 0, $last_space);
-        }
-
-        return rtrim($truncated) . $suffix;
-    }
-
-    /**
-     * Get artist title
-     *
-     * @param string $title The original title
-     * @return string The optimized title
-     */
-    protected function getArtistTitle(string $title):string
-    {
-        $city_terms = get_the_terms(get_the_ID(), BASE . 'city');
-        $city = ($city_terms && !is_wp_error($city_terms)) ? $city_terms[0]->name : 'Edmonton';
-
-        $artist_type = jvbArtistType(get_the_ID());
-
-        $title_format = "{$title} | {$city}'s Best {$artist_type}";
-        $title = strtok($title, ' ');
-		return (strlen($title_format) <= $this->character_limits['title']) ? $title_format : "{$title} | {$city}'s Best {$artist_type}";
-    }
-
-    /**
-     * Get artist description
-     *
-     * @param int $post_id The post ID
-     * @param Meta $meta The meta manager
-     * @return string The optimized description
-     */
-    protected function getArtistDescription(int $post_id, Meta $meta):string
-    {
-        $bio = $meta->get('short_bio');
-        if ($bio !== '') {
-            return $bio;
-        }
-
-        $first_name = $meta->get('first_name');
-
-        $city_terms = get_the_terms($post_id, BASE . 'city');
-        $city = ($city_terms && !is_wp_error($city_terms)) ? $city_terms[0]->name : 'Edmonton';
-
-        $artist_type = jvbArtistType($post_id);
-
-        // Get top styles if available
-        $styles = [];
-        $top_styles = $meta->get('top_style');
-        if (!empty($top_styles)) {
-            foreach ((array)$top_styles as $style_id) {
-                $style = get_term($style_id, BASE . 'style');
-                if ($style && !is_wp_error($style)) {
-                    $styles[] = $style->name;
-                }
-            }
-        }
-
-        // Get top themes if available
-        $themes = [];
-        $top_themes = $meta->get('top_theme');
-        if (!empty($top_themes)) {
-            foreach ((array)$top_themes as $theme_id) {
-                $theme = get_term($theme_id, BASE . 'theme');
-                if ($theme && !is_wp_error($theme)) {
-                    $themes[] = $theme->name;
-                }
-            }
-        }
-
-        // Fall back to all themes/styles if top ones aren't specified
-        if (empty($styles)) {
-            $style_terms = get_the_terms($post_id, BASE . 'style');
-            if ($style_terms && !is_wp_error($style_terms)) {
-                foreach (array_slice($style_terms, 0, 3) as $style) {
-                    $styles[] = $style->name;
-                }
-            }
-        }
-
-        if (empty($themes)) {
-            $theme_terms = get_the_terms($post_id, BASE . 'theme');
-            if ($theme_terms && !is_wp_error($theme_terms)) {
-                foreach (array_slice($theme_terms, 0, 3) as $theme) {
-                    $themes[] = $theme->name;
-                }
-            }
-        }
-
-        // Format styles and themes
-        $style_text = !empty($styles) ? implode(', ', array_slice($styles, 0, 3)) . ' styles' : '';
-        $theme_text = !empty($themes) ? implode(', ', array_slice($themes, 0, 3)) : '';
-
-        // Build description
-        $description = "Meet {$first_name}, an {$city} {$artist_type}";
-
-        if (!empty($theme_text)) {
-            $description .= " specializing in {$theme_text}";
-        }
-
-        if (!empty($style_text)) {
-            $description .= " with expertise in {$style_text}";
-        }
-
-        // Add shop information if available
-        $shop_terms = get_the_terms($post_id, BASE . 'shop');
-        if ($shop_terms && !is_wp_error($shop_terms)) {
-            $description .= ". Currently working at " . $shop_terms[0]->name;
-        }
-
-        return $description;
-    }
-
-    /**
-     * Get partner title
-     *
-     * @param string $title The original title
-     * @return string The optimized title
-     */
-    protected function getPartnerTitle(string $title):string
-    {
-        return "{$title} | Community Partner";
-    }
-
-    /**
-     * Get partner description
-     *
-     * @param int $post_id The post ID
-     * @param Meta $meta The meta manager
-     * @return string The optimized description
-     */
-    protected function getPartnerDescription(int $post_id, Meta$meta):string
-    {
-        $short_bio = $meta->get('short_bio');
-        if ($short_bio !== '') {
-            return $short_bio;
-        }
-        $established = $meta->get('established');
-
-        $description = get_the_title($post_id);
-
-        if (!empty($established)) {
-            $description .= " has been serving Edmonton since {$established}";
-        }
-
-        $description .= ". A proud community partner of edmonton.ink, supporting our local tattoo scene.";
-
-        return $description;
-    }
-
-    /**
-     * Get event title
-     *
-     * @param int $post_id The post ID
-     * @param string $title The original title
-     *
-     * @return string The optimized title
-     * @throws DateMalformedStringException
-     */
-    protected function getEventTitle(int $post_id, string $title):string
-    {
-        $meta = Meta::forPost($post_id);
-
-        // Get event type if available
-        $event_type = $meta->get('event_type') ?: '';
-        if ($event_type && term_exists((int)$event_type, BASE . 'type')) {
-            $event_type = get_term($event_type, BASE . 'type')->name;
-        }
-
-        // Get date information
-        $date_start = $meta->get('date_start');
-        $month = '';
-        if ($date_start) {
-            $date = new DateTime($date_start);
-            $month = $date->format('F');
-        }
-
-        if (!empty($title) && $title !== 'Auto Draft') {
-            if ($month) {
-                return "{$title} | {$month} Edmonton Tattoo Events";
-            } else {
-                return "{$title} | Edmonton Tattoo Event";
-            }
-        } elseif (!empty($event_type)) {
-            if ($month) {
-                return "Edmonton {$event_type} | {$month} Tattoo Event";
-            } else {
-                return "Edmonton {$event_type} | Tattoo Event";
-            }
-        } else {
-            if ($month) {
-                return "Edmonton Tattoo Event | {$month}";
-            } else {
-                return "Edmonton Tattoo Event";
-            }
-        }
-    }
-
-    /**
-     * Get event description
-     *
-     * @param int $post_id The post ID
-     *
-     * @return string The optimized description
-     * @throws DateMalformedStringException
-     */
-    protected function getEventDescription(int $post_id):string
-    {
-        $meta = Meta::forPost($post_id);
-        $title = get_the_title($post_id);
-
-        // Get event type if available
-        $event_type = $meta->get('event_type') ?: '';
-        if ($event_type && term_exists((int)$event_type, BASE . 'type')) {
-            $event_type = get_term($event_type, BASE . 'type')->name;
-        }
-
-        // Get date information
-        $date_start = $meta->get('date_start');
-        $date_format = '';
-        if ($date_start) {
-            $date = new DateTime($date_start);
-            $date_format = $date->format('F j, Y');
-        }
-
-        // Get location information
-        $location = $meta->get('location');
-        $location_name = '';
-        if (!empty($location['shop'])) {
-            $shop_term = get_term($location['shop'], BASE . 'shop');
-            if ($shop_term && !is_wp_error($shop_term)) {
-                $location_name = $shop_term->name;
-            }
-        } elseif (!empty($location['custom_location']['address'])) {
-            $location_name = $location['custom_location']['address'];
-        }
-
-        // Build description
-        $description = "Join us for";
-
-        if (!empty($title) && $title !== 'Auto Draft') {
-            $description .= " {$title}";
-        } elseif (!empty($event_type)) {
-            $description .= " this {$event_type} event";
-        } else {
-            $description .= " this exciting tattoo event";
-        }
-
-        if (!empty($date_format)) {
-            $description .= " on {$date_format}";
-        }
-
-        if (!empty($location_name)) {
-            $description .= " at {$location_name}";
-        }
-
-        // Add event details if available
-        $is_free = $meta->get('is_free');
-        if ($is_free) {
-            $description .= ". Free admission";
-        } else {
-            $cost = $meta->get('cost');
-            if ($cost) {
-                $description .= ". Admission: {$cost}";
-            }
-        }
-
-        $description .= ". Find more Edmonton tattoo events at edmonton.ink.";
-
-        return $description;
-    }
-
-    /**
-     * Get shop title
-     *
-     * @param WP_Term $term The term object
-     * @param Meta $meta The meta manager
-     * @return string The optimized title
-     */
-    protected function getShopTitle(WP_Term $term, Meta $meta):string
-    {
-        $city_id = $meta->get('city');
-        $city = 'Edmonton';
-
-        if ($city_id && term_exists((int)$city_id, BASE . 'city')) {
-            $city_term = get_term($city_id, BASE . 'city');
-            if ($city_term && !is_wp_error($city_term)) {
-                $city = $city_term->name;
-            }
-        }
-
-        $length = strlen(html_entity_decode($term->name)) + strlen($city);
-
-        $title = match (true) {
-            $length < 36 => $city . '\s Best Tattoo Studios',
-            $length < 38 => $city . '\s Best Tattoo Shops',
-            $length < 44 => $city . ' Tattoo Studio',
-            $length < 46 => $city . ' Tattoo Shop',
-            default => 'Tattoo Shop: ',
-        };
-		$name = html_entity_decode($term->name);
-        return "{$name} | {$title}";
-    }
-
-    /**
-     * Get shop description
-     *
-     * @param WP_Term $term The term object
-     * @param Meta $meta The meta manager
-     * @return string The optimized description
-     */
-    protected function getShopDescription(WP_Term $term, Meta $meta):string
-    {
-        $short_bio = $meta->get('short_bio');
-        if ($short_bio !== '') {
-            return $short_bio;
-        }
-
-        $established = $meta->get('established');
-        // Get city
-        $city_id = $meta->get('city');
-        $city = 'Edmonton';
-
-        if ($city_id && term_exists((int)$city_id, BASE . 'city')) {
-            $city_term = get_term($city_id, BASE . 'city');
-            if ($city_term && !is_wp_error($city_term)) {
-                $city = $city_term->name;
-            }
-        }
-
-        // Get artists
-        $artists = get_posts([
-            'post_type' => BASE . 'artist',
-            'tax_query' => [
-                [
-                    'taxonomy' => BASE . 'shop',
-                    'field' => 'term_id',
-                    'terms' => $term->term_id
-                ]
-            ],
-            'posts_per_page' => 3
-        ]);
-
-        $artist_names = [];
-        foreach ($artists as $artist) {
-            $artist_names[] = get_the_title($artist->ID);
-        }
-
-        // Build description
-        $description = html_entity_decode($term->name);
-
-        if (!empty($established)) {
-            $description .= " has been slinging ink in {$city} since {$established}";
-        } else {
-            $description .= " is a premier tattoo shop in {$city}";
-        }
-
-        if (!empty($artist_names)) {
-            $description .= ", featuring " . implode(', ', $artist_names);
-        }
-
-        $description .= ". Book your next tattoo today.";
-
-        return $description;
-    }
-
-    /**
-     * Get style title
-     *
-     * @param WP_Term $term The term object
-     * @return string The optimized title
-     */
-    protected function getStyleTitle(WP_Term $term):string
-    {
-		$name = html_entity_decode($term->name);
-        return "Edmonton's Best {$name} Tattoo Artists";
-    }
-
-    /**
-     * Get style description
-     *
-     * @param WP_Term $term The term object
-     * @param Meta $meta The meta manager
-     * @return string The optimized description
-     */
-    protected function getStyleDescription(WP_Term $term, Meta $meta):string
-    {
-        $tagline = $meta->get('tagline');
-
-        if (!$tagline !== '') {
-            return $tagline;
-        }
-        $characteristics = $meta->get('characteristics');
-        $alternate_names = $meta->get('alternate_name');
-
-        // Get alt names if available
-        $alt_name_text = '';
-        if (!empty($alternate_names)) {
-            $names = [];
-            foreach ($alternate_names as $alt) {
-                if (!empty($alt['name'])) {
-                    $names[] = $alt['name'];
-                }
-            }
-
-            if (!empty($names)) {
-                $alt_name_text = " (also known as " . implode(', ', array_slice($names, 0, 2)) . ")";
-            }
-        }
-
-        // Build description
-		$name = html_entity_decode($term->name);
-        $description = "{$name}{$alt_name_text} is a distinctive tattoo style";
-
-        if (!empty($characteristics)) {
-            $stripped = wp_strip_all_tags($characteristics);
-            if (strlen($stripped) > 50) {
-                $description .= ". " . mb_substr($stripped, 0, 100) . "...";
-            } else {
-                $description .= ". " . $stripped;
-            }
-        }
-
-		$name = html_entity_decode($term->name);
-        $description .= " Find Edmonton artists specializing in {$name} tattoos.";
-
-        return $description;
-    }
-
-    /**
-     * Get theme title
-     *
-     * @param WP_Term $term The term object
-     * @return string The optimized title
-     */
-    protected function getThemeTitle(WP_Term $term):string
-    {
-		$name = html_entity_decode($term->name);
-        return "Edmonton's Best {$name} Tattoos";
-    }
-
-    /**
-     * Get theme description
-     *
-     * @param WP_Term $term The term object
-     * @param Meta $meta The meta manager
-     * @return string The optimized description
-     */
-    protected function getThemeDescription(WP_Term $term, Meta $meta):string
-    {
-        $description_meta = $meta->get('description');
-        if ($description_meta !== '') {
-            return $description_meta;
-        }
-
-        // Get similar themes if available
-        $similar = $meta->get('similar');
-        $similar_text = '';
-
-        if (!empty($similar)) {
-            $similar_names = [];
-            foreach ((array)$similar as $similar_id) {
-                $similar_term = get_term($similar_id, BASE . 'theme');
-                if ($similar_term && !is_wp_error($similar_term)) {
-                    $similar_names[] = $similar_term->name;
-                }
-            }
-
-            if (!empty($similar_names)) {
-                $similar_text = " Similar themes include " . implode(', ', array_slice($similar_names, 0, 2)) . ".";
-            }
-        }
-
-        // Build description
-		$name = html_entity_decode($term->name);
-        $description = "Explore {$name} tattoos";
-
-
-        $description .= ", a popular motif in Edmonton's tattoo scene.";
-
-
-        $description .= $similar_text;
-		$name = html_entity_decode($term->name);
-        $description .= " Find artists specializing in {$name} tattoos.";
-
-        return $description;
-    }
-
-    /**
-     * Get city title
-     *
-     * @param WP_Term $term The term object
-     * @return string The optimized title
-     */
-    protected function getCityTitle(WP_Term $term):string
-    {
-		$name = html_entity_decode($term->name);
-        return "{$name} Tattoo Artists & Shops | edmonton.ink";
-    }
-
-    /**
-     * Get city description
-     *
-     * @param WP_Term $term The term object
-     * @return string The optimized description
-     */
-    protected function getCityDescription(WP_Term $term):string
-    {
-        // Count shops and artists in this city
-        $shop_count = 0;
-        $shops = get_terms([
-            'taxonomy' => BASE . 'shop',
-            'meta_key' => BASE . 'city',
-            'meta_value' => $term->term_id,
-            'fields' => 'count'
-        ]);
-
-        if (!is_wp_error($shops)) {
-            $shop_count = $shops;
-        }
-
-        $artist_count = 0;
-        $artists = get_posts([
-            'post_type' => BASE . 'artist',
-            'tax_query' => [
-                [
-                    'taxonomy' => BASE . 'city',
-                    'field' => 'term_id',
-                    'terms' => $term->term_id
-                ]
-            ],
-            'fields' => 'ids',
-            'posts_per_page' => -1
-        ]);
-
-        if (is_array($artists)) {
-            $artist_count = count($artists);
-        }
-
-        // Build description
-		$name = html_entity_decode($term->name);
-        $description = "Discover {$name}'s vibrant tattoo scene";
-
-        if ($shop_count > 0 || $artist_count > 0) {
-            $description .= " featuring";
-
-            $parts = [];
-            if ($shop_count > 0) {
-                $parts[] = "{$shop_count} local " . ($shop_count == 1 ? 'shop' : 'shops');
-            }
-
-            if ($artist_count > 0) {
-                $parts[] = "{$artist_count} talented " . ($artist_count == 1 ? 'artist' : 'artists');
-            }
-
-            $description .= " " . implode(' and ', $parts);
-        }
-
-        $description .= ". Find top local talent and book your next tattoo today.";
-
-        return $description;
-    }
-}
-
-//new SEOMetaManager();
diff --git a/inc/managers/SchemaManager.php b/inc/managers/SchemaManager.php
deleted file mode 100644
index d88016d..0000000
--- a/inc/managers/SchemaManager.php
+++ /dev/null
@@ -1,1234 +0,0 @@
-<?php
-namespace JVBase\managers;
-
-use JVBase\meta\Meta;
-use WP_Query;
-
-if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
-}
-/**
- * @deprecated use JVBase\managers\seo\SEO.php
- * Schema.org Generator for edmonton.ink
- *
- * This class generates structured schema.org data for better SEO
- * and search engine understanding of content.
- *
- * It integrates with the existing breadcrumb system to ensure
- * consistency between visual breadcrumbs and schema markup.
- */
-class SchemaManager
-{
-    protected array $content;
-    protected array $taxonomies;
-    protected array $directories;
-    /**
-     * Initialize the generator
-     */
-    public function __construct()
-    {
-        $this->directories = jvbGlobalDirectoryInfo();
-
-
-        // Output schema in the head
-        add_action('wp_head', [$this, 'outputSchema'], 1);
-
-        // Disable The SEO Framework schema for our custom content
-        add_filter('the_seo_framework_schema_graph_data', [$this, 'disableTSFSchema'], 10, 2);
-
-        // Make the getBreadcrumbSchema method available for the visual breadcrumbs
-        add_filter('jvb_breadcrumb_schema', [$this, 'getBreadcrumbSchema']);
-        add_action('init', [$this, 'init']);
-    }
-
-    /**
-     * @return void
-     */
-    public function init():void
-    {
-        // Filter post types
-        $this->content = array_keys(JVB_CONTENT);
-
-        // Filter term taxonomies
-        $this->taxonomies = array_keys(JVB_TAXONOMY);
-    }
-
-    /**
-     * Disable The SEO Framework schema on our custom content
-     * @param array $graph
-     * @param array|null $args
-     *
-     * @return array
-     */
-    public function disableTSFSchema(array $graph, array|null $args):array
-    {
-        $tsf = tsf();
-
-        if (null === $args) {
-            // We're in the loop.
-            if ($tsf->query()->is_real_front_page()) {
-                return [];
-            }
-            if ($tsf->query()->is_singular() && in_array($tsf->query()->get_current_post_type(), array_map(function ($content) { return jvbCheckBase($content); }, array_keys(JVB_CONTENT)))) {
-                return [];
-            } elseif ($tsf->query()->is_tax() && in_array($tsf->query()->get_current_taxonomy(), [
-                    BASE.'style',
-                    BASE.'type',
-                    BASE.'theme',
-                    BASE.'city',
-                    BASE.'artstyle',
-                    BASE.'arttheme',
-                    BASE.'artform',
-                    BASE.'artmedium',
-                    BASE.'pstyle',
-                ])) {
-                return [];
-            }
-        }
-        return $graph;
-    }
-
-    /**
-     * Output schema based on current context
-     * @return void
-     */
-    public function outputSchema():void
-    {
-        // Base schema that's included on every page
-        $schema = [
-            '@context' => 'https://schema.org',
-            '@graph' => [
-                $this->getWebsiteSchema(),
-            ]
-        ];
-
-        // Add context-specific schema
-        if (jvbIsDirectory()) {
-            $schema['@graph'][] = $this->getDirectorySchema();
-        } elseif (is_front_page()) {
-            $schema['@graph'][] = $this->getHomeSchema();
-        } elseif (is_singular(BASE.'artist')) {
-            $schema['@graph'][] = $this->getArtistSchema(get_the_ID());
-        } elseif (is_singular(BASE.'partner')) {
-            $schema['@graph'][] = $this->getPartnerSchema(get_the_ID());
-        } elseif (is_tax(BASE.'shop')) {
-            $schema['@graph'][] = $this->getShopSchema(get_queried_object_id());
-        } elseif (is_tax(BASE.'style')) {
-            $schema['@graph'][] = $this->getStyleSchema(get_queried_object_id());
-        } elseif (is_tax(BASE.'theme')) {
-            $schema['@graph'][] = $this->getThemeSchema(get_queried_object_id());
-        } elseif (is_front_page()) {
-            $schema['@graph'][] = $this->getHomeSchema();
-        }
-
-        // Add breadcrumbs to all pages
-        $breadcrumbs = $this->getBreadcrumbSchema();
-        if (!empty($breadcrumbs)) {
-            $schema['@graph'][] = $breadcrumbs;
-        }
-        echo '<!-- Custom Schema by Jake -->';
-        echo '<script type="application/ld+json">';
-        echo wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
-        echo '</script>';
-    }
-
-    /**
-     * Get base Website schema
-     * @return array Schema data
-     */
-    private function getWebsiteSchema():array
-    {
-        return [
-            '@type'         => 'WebSite',
-            '@id'           => get_home_url() . '/#website',
-            'name'          => get_bloginfo('name'),
-            'url'           => get_home_url(),
-            'description'   => 'Your tattoo scene on your screen.',
-            'inLanguage'    => 'en-CA',
-            'publisher'     => $this->getLegacyOrganizationReference(),
-        ];
-    }
-
-    /**
-     * Get Legacy Tattoo Removal reference for publisher
-     * This is critical for SEO to link back to the main business
-     * @return array Schema data
-     */
-    private function getLegacyOrganizationReference():array
-    {
-        return [
-            '@type' => 'Organization',
-            '@id' => 'https://legacytattooremoval.ca/#organization',
-            'name' => 'Legacy Tattoo Removal',
-            'url' => 'https://legacytattooremoval.ca',
-            'sameAs' => [
-                'https://www.instagram.com/legacytattooremoval',
-                'https://www.facebook.com/legacytattooremoval',
-                'https://www.tiktok.com/@legacytattooremoval',
-                'https://bsky.app/profile/legacytattooremoval.ca'
-            ],
-            'logo' => [
-                '@type' => 'ImageObject',
-                'url' => 'https://legacytattooremoval.ca/wp-content/uploads/2024/09/legacy-tattoo-removal.webp'
-            ]
-        ];
-    }
-
-    /**
-     * Get relationship schema between edmonton.ink and Legacy
-     * @return array Schema data
-     */
-    private function getLegacyRelationshipSchema():array
-    {
-        return [
-            '@type' => 'Organization',
-            '@id' => get_home_url() . '/#organization',
-            'name' => 'edmonton.ink',
-            'url' => get_home_url(),
-            'memberOf' => [
-                '@id' => 'https://edmonton.ink/#organization',
-                'publisher' => [
-                    '@id' => 'https://legacytattooremoval.ca/#organization'
-                ]
-            ],
-        ];
-    }
-
-    /**
-     * Get Breadcrumb schema
-     * @return array Schema data
-     */
-    public function getBreadcrumbSchema():array
-    {
-        // Use the existing jvbGetCrumbs function
-        $crumbs = jvbGetCrumbs();
-
-        if (empty($crumbs)) {
-            return [];
-        }
-
-        // Format for schema
-        $items = [];
-        foreach ($crumbs as $index => $crumb) {
-            // Make sure we have a URL, even if it's just a placeholder for current page
-            $url = (!empty($crumb['url'])) ? $crumb['url'] : get_permalink();
-
-            // For icon-only breadcrumbs, use the name property
-            $name = $crumb['name'];
-            if (isset($crumb['icon']) && empty(strip_tags($name))) {
-                $name = strip_tags($crumb['name']);
-            }
-
-            $items[] = [
-                '@type' => 'ListItem',
-                'position' => $index + 1,
-                'name' => $name,
-                'item' => $url
-            ];
-        }
-
-        return [
-            '@type' => 'BreadcrumbList',
-            '@id' => get_permalink() . '#breadcrumb',
-            'itemListElement' => $items
-        ];
-    }
-
-    /**
-     * Get Artist schema
-     * @param int $post_id The artist post ID
-     * @return array Schema data
-     */
-    private function getArtistSchema(int $post_id):array
-    {
-        $meta = Meta::forPost($post_id);
-		$metaValues = $meta->getAll();
-
-        $permalink = get_permalink($post_id);
-
-        // Get artist data
-        $name = get_the_title($post_id);
-        $first_name = $meta->get('first_name');
-        $bio = $meta->get('bio');
-        $short_bio = $meta->get('short_bio');
-        $description = $short_bio ?: wp_strip_all_tags($bio) ?: get_the_excerpt($post_id);
-
-        // Build person schema
-        $schema = array_merge(
-            [
-                '@type' => 'Person',
-                '@id' => $permalink . '#person',
-                'name' => $name,
-                'givenName' => $first_name,
-                'description' => $description,
-                'url' => $permalink,
-                'jobTitle' => jvbArtistType($post_id),
-                'memberOf' => [
-                    '@id' => 'https://edmonton.ink/#organization'
-                ]
-            ],
-            $this->getContact($metaValues),
-            $this->getLinks($metaValues),
-            $this->getRatings($metaValues),
-            $this->getReviews($metaValues),
-            $this->getAlternateName($metaValues),
-            $this->getCities($metaValues),
-            $this->getSpecialties($metaValues),
-            $this->getRate($metaValues),
-            $this->getAwards($metaValues),
-            $this->getServices($metaValues),
-            $this->getLanguages($metaValues),
-            $this->getCredentials($metaValues),
-            $this->getKeywords($metaValues),
-        );
-
-        // Add image if available
-        $image_id = $metaValues['image_portrait']??false;
-        if ($image_id) {
-            $image_url = wp_get_attachment_image_url($image_id, 'full');
-            if ($image_url) {
-                $schema['image'] = $image_url;
-            }
-        }
-
-        // Add shop affiliation
-        $shops = get_the_terms($post_id, BASE.'shop');
-        if (!empty($shops) && !is_wp_error($shops)) {
-            $worksFor = [];
-            foreach ($shops as $shop) {
-                $worksFor[] = [
-                    '@type' => 'Organization',
-                    'name' => $shop->name,
-                    'url' => get_term_link($shop),
-                    'memberOf' => [
-                        '@id' => 'https://edmonton.ink.ca/#organization'
-                    ]
-                ];
-            }
-            $schema['worksFor'] = count($worksFor) == 1 ? $worksFor[0] : $worksFor;
-        }
-
-        // Add styles as skills
-        $styles = get_the_terms($post_id, BASE.'style');
-        if (!empty($styles) && !is_wp_error($styles)) {
-            $skills = [];
-            foreach ($styles as $style) {
-                $skills[] = $style->name;
-            }
-            if (!empty($skills)) {
-                $schema['knowsAbout'] = $skills;
-            }
-        }
-
-        // Add latest portfolio works
-        $work = get_posts([
-            'post_type'         => [BASE.'tattoo', BASE.'piercing', BASE.'artwork'],
-            'author'            => get_post_meta($post_id, BASE . 'link', true),
-            'posts_per_page'    => 10
-        ]);
-        if (!empty($work)) {
-            $works = [];
-            foreach ($work as $w) {
-                $img = get_post_thumbnail_id($w->ID);
-                $imgURL = wp_get_attachment_image_url($img, 'full');
-
-                $works[] = [
-                    '@type'     => 'CreativeWork',
-                    'name'      => $w->post_title?:str_replace(BASE, '', $w->post_type).' by '.$name,
-                    'image'     => $imgURL?: '',
-                    'url'       => get_permalink($w->ID),
-                    'datePublished' => get_the_date('c', $w->ID),
-                ];
-            }
-
-            if (!empty($works)) {
-                $schema['creator']  = $works;
-            }
-        }
-
-        return $schema;
-    }
-
-    /**
-     * Get Shop schema
-     * @param int $term_id The shop term ID
-     * @return array Schema data
-     */
-    private function getShopSchema(int $term_id): array
-    {
-        $meta = Meta::forTerm($term_id);
-		$metaValues = $meta->getAll();
-        $term = get_term($term_id, BASE.'shop');
-        $permalink = get_term_link($term_id, BASE.'shop');
-
-        // Basic shop information
-        $schema = array_merge(
-            [
-                '@type'             => 'LocalBusiness',
-                '@id'               => $permalink . '#organization',
-                'name'              => html_entity_decode($term->name),
-                'description'       => $meta->get('short_bio') ?: $term->description,
-                'url'               => $permalink,
-                'priceRange'        => '$$', // Default price range
-                'additionalType'    => 'https://schema.org/TattooParlor', // Custom business type
-                'memberOf'          => [
-                    '@id' => 'https://edmonton.ink/#organization'
-                ]
-            ],
-            $this->getAlternateName($metaValues),
-            $this->getLinks($metaValues),
-            $this->getSlogan($metaValues),
-            $this->getContact($metaValues),
-            $this->getLocation($metaValues),
-            $this->getCities($metaValues),
-            $this->getHours($metaValues),
-            $this->getSpecialties($metaValues),
-            $this->getRate($metaValues),
-            $this->getAwards($metaValues),
-            $this->getRatings($metaValues),
-            $this->getReviews($metaValues),
-            $this->getServices($metaValues),
-            $this->getLanguages($metaValues),
-            $this->getPaymentAccepted($metaValues),
-            $this->getAmenities($metaValues),
-            $this->getCredentials($metaValues),
-            $this->getKeywords($metaValues),
-            $this->getDissolutionDate($metaValues),
-        );
-
-        // Add founding date if available
-        $established = $metaValues['established'];
-        if ($established) {
-            $schema['foundingDate'] = $established . '-01-01'; // Approximate to January 1st
-        }
-
-        // Add image
-        $image_id = $metaValues['image'];
-        if ($image_id) {
-            $image_url = wp_get_attachment_image_url($image_id, 'full');
-            if ($image_url) {
-                $schema['image'] = $image_url;
-                $schema['logo'] = $image_url;
-            }
-        }
-
-        // Get artists working at this shop
-        $artists = $this->getArtists($term_id, BASE.'shop');
-        if (!empty($artists)) {
-            $schema['employee'] = $artists;
-        }
-
-        return $schema;
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getLocation(array $meta):array
-    {
-        // Add location data
-        $location = $meta['location'];
-        $schema = [];
-        if ($location && !empty($location['address'])) {
-            $schema['address'] = [
-                '@type' => 'PostalAddress',
-                'streetAddress' => $location['address']
-            ];
-
-            if (!empty($location['lat']) && !empty($location['lng'])) {
-                $schema['geo'] = [
-                    '@type' => 'GeoCoordinates',
-                    'latitude' => $location['lat'],
-                    'longitude' => $location['lng']
-                ];
-            }
-        }
-
-        return $schema;
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getContact(array $meta):array
-    {
-        $contact = $meta['public_contact']??'';
-        if (!is_array($contact)) {
-            $contact = explode(',', $contact);
-        }
-        $return = [];
-        if (!empty($contact)) {
-            foreach ($contact as $c) {
-                switch ($c) {
-                    case 'text':
-                    case 'call':
-                        $phone = $meta['phone']??'';
-                        if ($phone !== '') {
-                            $return['telephone'] = $phone;
-                        }
-                        break;
-                    case 'email':
-                        $email = $meta['email']??'';
-                        if ($email !== '') {
-                            $return['email'] = $email;
-                        }
-                        break;
-                }
-            }
-        }
-
-        return $return;
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getAlternateName(array $meta):array
-    {
-        return [
-            'alternateName' => $meta['alternate_name']
-        ];
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getCities(array $meta):array{
-        $get = $meta['city'];
-        $areas = [];
-        if (!is_array($get)) {
-            $get = explode(',', $get);
-        }
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $city = get_term($g, BASE.'city');
-                if (!$city || is_wp_error($city)) {
-                    return [];
-                }
-                $areas[] = [
-                    '@type' => 'City',
-                    'name'  => $city->name,
-                    '@id'   => get_term_meta($city->term_id, BASE.'wiki')
-                ];
-            }
-            if (!empty($areas)) {
-                $areas = [
-                    'areaServed' => $areas
-                ];
-            }
-        }
-
-        return $areas;
-    }
-
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getLinks(array $meta):array
-    {
-        $get = $meta['links']??[];
-        $links = [];
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $links[] = $g['url'];
-            }
-            if (!empty($links)) {
-                $links = [
-                    'sameAs'    => $links
-                ];
-            }
-        }
-
-        return $links;
-    }
-
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getSpecialties(array $meta):array
-    {
-        $specialties = [];
-        $get = $meta['specialties']??[];
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $temp = [
-                    '@type' => 'Specialty',
-                    'name' => $g['specialty']
-                ];
-                if ($g['description'] !== '') {
-                    $temp['description'] = $g['description'];
-                }
-                $specialties[] = $temp;
-            }
-
-            if (!empty($specialties)) {
-                $specialties = [
-                    'specialties'   => $specialties
-                ];
-            }
-        }
-
-
-        return $specialties;
-    }
-
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getRate(array $meta):array
-    {
-        $get = $meta['rate']??0;
-        if ($get > 0) {
-            return [
-                '@type' => 'PropertyValue',
-                'name'  => 'Shop Rate (per hour)',
-                'value' => $get
-            ];
-        }
-        return [];
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getAwards(array $meta):array
-    {
-        $get = $meta['awards']??[];
-        $awards = [];
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $awards[] = [
-                    '@type' => 'Award',
-                    'name'  => $g['name'],
-                    'description'   => 'Voted '.$g['name'].' at the '.$g['year'].' '.$g['presenter'],
-                    'dateAwarded'   =>$g['year'],
-                ];
-            }
-            if (!empty($awards)) {
-                $awards = [
-                    'awards'    => $awards
-                ];
-            }
-        }
-
-        return $awards;
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getRatings(array $meta):array
-    {
-        $out = [];
-        $rating = $meta['average_rating']??'none';
-        if ($rating !== 'none') {
-            $total = $meta['total_ratings']??0;
-
-            $out = [
-                'aggregateRating'   => [
-                    '@type' =>'AggregateRating',
-                    'ratingValue'   =>$rating,
-                    'ratingCount'   => ($total === '') ? 1 : $total,
-                    'bestRating'    => 5,
-                    'worstRating'   => 1,
-                ]
-            ];
-        }
-
-        return $out;
-    }
-
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getReviews(array $meta):array
-    {
-        $get = $meta['reviews'];
-        $reviews = [];
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $temp = [
-                    '@type' => 'Review',
-                    'author'    => [
-                        '@type' => 'Person',
-                        'name'  => $g['name']
-                    ],
-                    'datePublished' => $g['date'],
-                    'reviewBody'    =>$g['review'],
-                ];
-                if ($g['rating'] !== 'none') {
-                    $temp['reviewRating'] = [
-                        '@type' =>'ReviewRating',
-                        'ratingValue'   => $g['rating'],
-                        'bestRating'    => 5,
-                        'worstRating'   => 1,
-                    ];
-                }
-                if ($g['url'] !== '') {
-                    $temp['url']= $g['url'];
-                }
-
-                $reviews[] = $temp;
-            }
-            if (!empty($reviews)) {
-                $reviews = [
-                    'review'    => $reviews
-                ];
-            }
-        }
-
-        return $reviews;
-    }
-
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getServices(array $meta):array
-    {
-        $get = $meta['services']??[];
-        $services = [];
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $temp = [
-                    '@type' => 'Service',
-                    'itemOffered'   =>[
-                        '@type' =>'Service',
-                        'name'  => $g['name']
-                    ]
-                ];
-                if ($g['description'] !== '') {
-                    $temp['itemOffered']['description'] = $g['description'];
-                }
-                $services[] = $temp;
-            }
-            if (!empty($services)) {
-                $services = [
-                    '@type' => 'OfferCatalog',
-                    'name'  => 'Services',
-                    'itemListElement'   => $services
-                ];
-            }
-        }
-
-        return $services;
-    }
-
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getLanguages(array $meta):array
-    {
-        $get = $meta['languages']??[];
-        $languages = [];
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $languages[] = [
-                    '@type' => 'Language',
-                    'name'  => $g['language']
-                ];
-            }
-            if (!empty($languages)) {
-                $languages = [
-                    'availableLanguage' => $languages
-                ];
-            }
-        }
-
-
-        return $languages;
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getKeywords(array $meta):array
-    {
-        $get = $meta['keywords'];
-        $keywords = [
-            'Edmonton Tattoo Artist',
-            'Tattoo',
-            'Edmonton Tattoo'
-        ];
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $keywords[] = $g['keyword'];
-            }
-        }
-
-        if (!empty($keywords)) {
-            $keywords = [
-                'keywords'  => $keywords
-            ];
-        }
-        return $keywords;
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getHours(array $meta):array
-    {
-        $hours = $meta['hours']??[];
-        $return = [];
-        if (!empty($hours)) {
-            foreach ($hours as $hour) {
-                $days = jvbCondenseDayRange(explode(',', $hour['days']));
-                $return[] = $days.' '.$hour['time_open'].'-'.$hour['time_closes'];
-            }
-        }
-
-        return [
-            'openingHours'  => $return
-        ];
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getSlogan(array $meta):array
-    {
-		if (!array_key_exists('slogan', $meta) || $meta['slogan'] === ''){
-			return [];
-		}
-        return [
-            'slogan'    => $meta['slogan']
-        ];
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getPaymentAccepted(array $meta):array
-    {
-        return [
-            'paymentAccepted' => $meta['payment']
-        ];
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getAmenities(array $meta):array
-    {
-        $amenities = [];
-        $get = $meta['amenities']??[];
-        if (!is_array($get)) {
-            $get = explode(',', $get);
-        }
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $amenities[] = [
-                    '@type' => 'LocationFeatureSpecification',
-                    'name'  => $g,
-                    'value' => true,
-                ];
-            }
-            if (!empty($amenities)) {
-                $amenities = [
-                    'amenityFeatures' => $amenities
-                ];
-            }
-        }
-
-        return $amenities;
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getCredentials(array $meta):array
-    {
-        $health = [
-            'hasHealthAspect'   => [
-                '@type'         => 'HealthAspect',
-                'name'          => 'Safety Protocols',
-                'description'   => 'Following Alberta Health Services guidelines for tattoo safety and sterilization'
-            ]
-        ];
-
-        $credentials = [];
-        $get = $meta['credentials']??[];
-        if (!is_array($get)) {
-            $get = explode(',', $get);
-        }
-        if (!empty($get)) {
-            foreach ($get as $g) {
-                $credentials[] = [
-                    '@type' => 'EducationalOccupationalCredential',
-                    'name'                  => $g,
-                    'credentialCategory'    => 'Safety Certification',
-                ];
-            }
-            if (!empty($credentials)) {
-                $credentials = [
-                    'hasCredential' => $credentials
-                ];
-            }
-        }
-
-        return (empty($credentials)) ? $health : array_merge($health, $credentials);
-    }
-
-    /**
-     * @param array $meta
-     *
-     * @return array
-     */
-    private function getDissolutionDate(array $meta):array
-    {
-        if (array_key_exists('permanently_close', $meta)) {
-            return [
-                'dissolutionDate' => $meta['dissolution_date']
-            ];
-        }
-        return [];
-    }
-
-
-    /**
-     * @param int $termID
-     * @param string $taxonomy
-     *
-     * @return array
-     */
-    private function getArtists(int $termID, string $taxonomy):array
-    {
-        $artists = new WP_Query(array(
-            'post_type'  => BASE.'artist',
-            'posts_per_page' => -1,
-            'orderby'       => 'title',
-            'tax_query'  => [
-                [
-                    'taxonomy'   => $taxonomy,
-                    'terms'      => $termID,
-                ]
-            ],
-            'fields'    => 'ids',
-        ));
-        $out = [];
-        if ($artists->have_posts()) {
-            foreach ($artists->posts as $artist) {
-                $url = get_permalink($artist);
-                $out[] = [
-                    '@type' => 'Person',
-                    '@id'   => $url,
-                    'name'  => get_the_title($artist),
-                    'url'   => $url,
-                ];
-            }
-        }
-        return $out;
-    }
-
-    /**
-     * Get style schema
-     * @param int $term_id The style term ID
-     * @return array Schema data
-     */
-    private function getStyleSchema(int $term_id):array
-    {
-        $meta = Meta::forTerm($term_id);
-        $term = get_term($term_id, BASE.'style');
-        $permalink = get_term_link($term_id, BASE.'style');
-
-        $schema = array_merge(
-            [
-                '@type' => 'CreativeWork',
-                '@id' => $permalink . '#style',
-                'name' => html_entity_decode($term->name),
-                'description' => $meta->get('characteristics') ?: $term->description,
-                'url' => $permalink,
-                'mainEntityOfPage' => [
-                    '@type' => 'WebPage',
-                    '@id' => $permalink
-                ]
-            ],
-            $this->getAlternateName($meta),
-        );
-
-        $artists = $this->getArtists($term_id, BASE.'style');
-
-        if (!empty($artists)) {
-            $schema['mentions'] = $artists;
-        }
-        return $schema;
-    }
-
-    /**
-     * Get Theme schema
-     * @param int $term_id The theme term ID
-     * @return array Schema data
-     */
-    private function getThemeSchema(int $term_id):array
-    {
-        $meta = Meta::forTerm($term_id);
-        $term = get_term($term_id, BASE.'theme');
-        $permalink = get_term_link($term_id, BASE.'theme');
-
-        $schema = [
-            '@type' => 'CreativeWork',
-            '@id' => $permalink . '#theme',
-            'name' => html_entity_decode($term->name),
-            'description' => $meta->get('description') ?: $term->description,
-            'url' => $permalink,
-            'mainEntityOfPage' => [
-                '@type' => 'WebPage',
-                '@id' => $permalink
-            ]
-        ];
-
-        $artists = $this->getArtists($term_id, BASE.'theme');
-
-        if (!empty($artists)) {
-            $schema['mentions'] = $artists;
-        }
-
-        return $schema;
-    }
-
-    /**
-     * Get Partner schema
-     * @param int $post_id The partner post ID
-     * @return array Schema data
-     */
-    private function getPartnerSchema(int $post_id):array
-    {
-        $meta = Meta::forPost($post_id);
-		$metaValues = $meta->getAll();
-        $permalink = get_permalink($post_id);
-
-        $schema = array_merge(
-            [
-                '@type' => 'Organization',
-                '@id' => $permalink . '#organization',
-                'name' => get_the_title($post_id),
-                'url' => $permalink,
-                'description' => get_the_excerpt($post_id),
-                'memberOf' => [
-                    '@id' => 'https://edmonton.ink/#organization'
-                ]
-            ],
-            $this->getAlternateName($metaValues),
-            $this->getLinks($metaValues),
-            $this->getSlogan($metaValues),
-            $this->getContact($metaValues),
-            $this->getLocation($metaValues),
-            $this->getCities($metaValues),
-            $this->getHours($metaValues),
-            $this->getSpecialties($metaValues),
-            $this->getRate($metaValues),
-            $this->getAwards($metaValues),
-            $this->getRatings($metaValues),
-            $this->getReviews($metaValues),
-            $this->getServices($metaValues),
-            $this->getLanguages($metaValues),
-            $this->getPaymentAccepted($metaValues),
-            $this->getAmenities($metaValues),
-            $this->getCredentials($metaValues),
-            $this->getKeywords($metaValues),
-            $this->getDissolutionDate($metaValues),
-        );
-
-        // Add image/logo
-        $image_id = $metaValues['image'];
-        if ($image_id) {
-            $image_url = wp_get_attachment_image_url($image_id, 'full');
-            if ($image_url) {
-                $schema['logo'] = $image_url;
-                $schema['image'] = $image_url;
-            }
-        }
-
-        return $schema;
-    }
-
-    /**
-     * Get Home page schema
-     * @return array Schema data
-     */
-    private function getHomeSchema():array
-    {
-        // Main dataset schema for homepage
-		return [
-			'@type' => 'WebPage',
-			'@id' => get_home_url() . '/#webpage',
-			'url' => get_home_url(),
-			'name' => get_bloginfo('name') . ' | Edmonton\'s Best Tattoo Artists',
-			'description' => 'Discover Edmonton\'s top tattoo artists, shops, and styles. Your comprehensive guide to Edmonton\'s tattoo scene.',
-			'isPartOf' => [
-				'@id' => get_home_url() . '/#website'
-			],
-			'about' => [
-				'@type' => 'Dataset',
-				'name' => 'Edmonton Tattoo Artist Directory',
-				'description' => 'Comprehensive directory of professional tattoo artists in Edmonton, Alberta',
-				'creator' => [
-					'@id' => 'https://legacytattooremoval.ca/#organization'
-				],
-				'publisher' => [
-					'@id' => 'https://legacytattooremoval.ca/#organization'
-				]
-			],
-		];
-    }
-
-    /**
-     * Adds enhanced schema support for directory pages
-     *
-     * @return array Schema data
-     */
-    private function getDirectorySchema():array
-    {
-        $current = jvbGetDirectoryInfo();
-		if (empty($current)){
-			return [];
-		}
-        if ($current['title'] === 'Map') {
-            return $this->getMapSchema();
-        }
-
-        // Base schema for collection page
-        $schema = [
-            '@type'       => 'CollectionPage',
-            '@id'         => $current['url'] . '#collectionpage',
-            'name'        => $current['title'] . ' Directory',
-            'description' => 'An alphabetical directory of '.strtolower($current['title']).'.',
-            'url'         => $current['url'],
-            'isPartOf'    => [
-                '@id' => get_home_url() . '/#website'
-            ],
-            'about'         => [
-                '@type' => 'Thing',
-                'name'  => $current['title'],
-            ],
-            'mainEntity'    => [
-                '@type'             => 'ItemList',
-                'itemListElement'   => []
-            ]
-        ];
-        $items = [];
-        switch ($current['slug']) {
-            case BASE.'artist':
-                $type = 'Person';
-                break;
-            case BASE.'event':
-                $type = 'Event';
-                break;
-            case BASE.'shop':
-            case BASE.'partner':
-                $type = 'LocalBusiness';
-                break;
-            default:
-                $type = 'Thing';
-                break;
-        }
-        if ($current['type'] === 'post') {
-            $args = [
-                'post_type'         => $current['slug'],
-                'posts_per_page'    => 50,
-                'orderby'           => 'title',
-                'order'             => 'ASC'
-            ];
-            $things = get_posts($args);
-
-
-            foreach ($things as $index => $thing) {
-                $items[] = [
-                    '@type'     => 'ListItem',
-                    'position'  => $index+1,
-                    'item'      => [
-                        '@type'     => $type,
-                        'name'      => $thing->post_title,
-                        'url'       => get_permalink($thing->ID)
-                    ]
-                ];
-            }
-        } elseif ($current['type'] === 'term') {
-            $things = get_terms([
-                'taxonomy'      => $current['slug'],
-                'hide_empty'    => true,
-                'number'        => 50,
-            ]);
-
-            foreach ($things as $index => $thing) {
-                $items[] = [
-                    '@type'     => 'ListItem',
-                    'position'  => $index+1,
-                    'item'      => [
-                        '@type'     => $type,
-                        'name'      => $thing->name,
-                        'url'       => get_term_link($thing)
-                    ]
-                ];
-            }
-        }
-
-        $schema['mainEntity']['itemListElement'] = $items;
-
-        return $schema;
-    }
-
-    /**
-     * Output schema for map page
-     *
-     * @return array Schema data
-     * TODO: Should we list 50 shops here?
-     */
-    private function getMapSchema():array
-    {
-        $map = $this->directories[get_the_ID()];
-        return [
-            '@type'       => 'MapLocation',
-            '@id'         => $map['url'] . '#map',
-            'name'        => 'Edmonton Tattoo Shops Map',
-            'description' => 'Interactive map of tattoo shops in Edmonton',
-            'url'         => $map['url'],
-            'hasMap'      => [
-                '@type'   => 'Map',
-                'mapType' => 'VenueMap'
-            ]
-        ];
-    }
-}
diff --git a/inc/managers/queue/executors/ContentExecutor.php b/inc/managers/queue/executors/ContentExecutor.php
index d9a8e11..4bc06de 100644
--- a/inc/managers/queue/executors/ContentExecutor.php
+++ b/inc/managers/queue/executors/ContentExecutor.php
@@ -449,7 +449,8 @@
 
 			foreach ($children as $child) {
 				$childMeta = Meta::forPost($child);
-				$result = $childMeta->setAll($values, false);
+				$childMeta->setAll($values, false);
+				$result = $childMeta->save();
 			}
 		}
 	}
diff --git a/inc/managers/queue/executors/ContentTermExecutor.php b/inc/managers/queue/executors/ContentTermExecutor.php
index 9703cbe..bac1179 100644
--- a/inc/managers/queue/executors/ContentTermExecutor.php
+++ b/inc/managers/queue/executors/ContentTermExecutor.php
@@ -85,12 +85,10 @@
 			}
 
 			// Update metadata
-			$results = $meta->setAll($setData);
+			$meta->setAll($setData);
+			$results = $meta->save();
 
 			if ($results) {
-				// Clear cache
-				JVB()->cache()->for($taxonomy)->delete("term_{$termID}_meta");
-
 				// Trigger any post-update actions (e.g., thumbnail generation)
 				do_action(BASE . "{$taxonomy}_updated", $termID, $userID, $setData);
 
diff --git a/inc/managers/queue/executors/UploadExecutor.php b/inc/managers/queue/executors/UploadExecutor.php
index 653fd31..1d42036 100644
--- a/inc/managers/queue/executors/UploadExecutor.php
+++ b/inc/managers/queue/executors/UploadExecutor.php
@@ -458,8 +458,11 @@
 			if (!$found) {
 				error_log('Could not find a gallery upload field for post '.$ID);
 			}
+
+			$meta->save();
 		}
 
+
 		return [
 			'ID'	=> $ID,
 			'usedUploads' => $uploadIds
@@ -664,6 +667,8 @@
 			$allIds = array_unique(array_merge($existingIds, $attachmentIds));
 			$meta->set($data['field_name'], implode(',', $allIds));
 		}
+
+		$meta->save();
 	}
 
 	private function updateFieldValue(array $data, array $results): void
@@ -683,6 +688,7 @@
 		$allIds = array_unique(array_merge($existingIds, $attachmentIds));
 
 		$meta->set($data['field_name'], implode(',', $allIds));
+		$meta->save();
 	}
 
 	private function getMetaManager(array $data): ?Meta
@@ -745,12 +751,14 @@
 		} elseif (str_starts_with($mimeType, 'video/')) {
 			$meta = Meta::forPost($postId);
 			$meta->set('video', $attachmentId);
+			$meta->save();
 		} else {
 			$meta = Meta::forPost($postId);
 			$existing = $meta->get('documents');
 			$existingIds = !empty($existing) ? explode(',', $existing) : [];
 			$existingIds[] = $attachmentId;
 			$meta->set('documents', implode(',', $existingIds));
+			$meta->save();
 		}
 	}
 
diff --git a/inc/rest/routes/NewsRoutes.php b/inc/rest/routes/NewsRoutes.php
index b0aedfc..8e30b42 100644
--- a/inc/rest/routes/NewsRoutes.php
+++ b/inc/rest/routes/NewsRoutes.php
@@ -393,6 +393,7 @@
                 $m = $meta->set($key, $value);
                 $result[$key] = $m;
             }
+			$meta->save();
         }
 
         $this->cache->flush();
diff --git a/inc/rest/routes/SettingsRoutes.php b/inc/rest/routes/SettingsRoutes.php
index 26b77ea..227def2 100644
--- a/inc/rest/routes/SettingsRoutes.php
+++ b/inc/rest/routes/SettingsRoutes.php
@@ -145,6 +145,8 @@
             }
             $this->cache->flush();
         }
+		$tempMeta->save();
+		$meta->save();
         return [
             'success'   => true,
             'result'   => $results,
diff --git a/inc/rest/routes/ShopRoutes.php b/inc/rest/routes/ShopRoutes.php
index 4f69614..83132a0 100644
--- a/inc/rest/routes/ShopRoutes.php
+++ b/inc/rest/routes/ShopRoutes.php
@@ -173,7 +173,8 @@
             }
 		}
 
-		$results = $meta->setAll($setData);
+		$meta->setAll($setData);
+		$results = $meta->save();
 
 //        $results = [];
 //
@@ -1383,8 +1384,10 @@
 
 
         $userMeta->set($uMeta, $shops);
+		$userMeta->save();
 
         $artistMeta->set($aMeta, $shops);
+		$artistMeta->save();
 
         $owners = $shopMeta->get($sMeta);
         $owners = ($owners === '') ? [] : explode(',', $shops);
@@ -1393,7 +1396,7 @@
         }
         $owners = implode(',', $owners);
         $shopMeta->set($sMeta, $owners);
-
+		$shopMeta->save();
         return true;
     }
 }
diff --git a/inc/rest/routes/UploadRoutes.php b/inc/rest/routes/UploadRoutes.php
index 4b2a4a6..f1db7a0 100644
--- a/inc/rest/routes/UploadRoutes.php
+++ b/inc/rest/routes/UploadRoutes.php
@@ -627,6 +627,7 @@
 
 		// Update with comma-separated string
 		$meta->set($data['field_name'], implode(',', $all_ids));
+		$meta->save();
 	}
 
 	/**
@@ -1124,10 +1125,6 @@
 		return $this->success($response);
 	}
 
-
-
-
-
 	public function handleGroupingRequest(WP_REST_Request $request): WP_REST_Response
 	{
 		try {
@@ -1199,687 +1196,4 @@
 			return $this->error('Grouping operation failed: '.$e->getMessage());
 		}
 	}
-
-	protected function processUploadGroups(WP_Error|array $result, object $operation, array $data): WP_Error|array
-	{
-		try {
-			$queue = JVB()->queue();
-			$all_uploaded_images = [];
-
-			// Get the upload operation ID from dependencies
-			$dependencies = json_decode($operation->dependencies, true);
-
-			if (empty($dependencies)) {
-				return [
-					'success'   => false,
-					'result'    => 'No dependencies found'
-				];
-			}
-
-			// Collect uploads from all dependency operations
-			$uploads = [];
-			foreach ($dependencies as $dependency) {
-				// Use getOperationValue like ContentRoutes does
-				$res = $queue->getOperationValue($dependency, 'result');
-
-				if (empty($res)) {
-					continue;
-				}
-
-				// Results are stored at root level, keyed by upload_id
-				// Filter to only include actual upload results (arrays with attachment_id)
-				foreach ($res as $key => $value) {
-					if (is_array($value) && isset($value['attachment_id'])) {
-						$uploads[$key] = $value;
-					}
-				}
-			}
-
-			if (empty($uploads)) {
-				return [
-					'success'   => false,
-					'result'    => 'No uploads to process'
-				];
-			}
-
-			// Build map of upload_id => attachment_id
-			foreach ($uploads as $upload_id => $img) {
-				$all_uploaded_images[$upload_id] = [
-					'upload_id' => $upload_id,
-					'attachment_id' => (int)$img['attachment_id']
-				];
-			}
-
-			$content = jvbCheckBase($data['content']);
-			if (Features::forContent($data['content'])->has('is_timeline')) {
-				return $this->processTimelineUploads($data, $uploads, $all_uploaded_images, $operation);
-			}
-
-			$user = (int)$data['user'];
-			$created_posts = [];
-			$used_upload_ids = [];
-
-			// Create posts from groups
-			foreach ($data['posts'] as $index => $post) {
-				$post_title = !empty($post['fields']['post_title'])
-					? sanitize_text_field($post['fields']['post_title'])
-					: 'New ' . JVB_CONTENT[$data['content']]['singular'] . ' ' . ($index + 1);
-
-				$post_excerpt = !empty($post['fields']['post_excerpt'])
-					? sanitize_textarea_field($post['fields']['post_excerpt'])
-					: '';
-
-				$args = [
-					'post_type' => $content,
-					'post_author' => $user,
-					'post_status' => 'draft',
-					'post_title' => $post_title,
-					'post_excerpt' => $post_excerpt,
-				];
-
-				$new_post_id = wp_insert_post($args);
-
-				if ($new_post_id && !is_wp_error($new_post_id)) {
-					$created_posts[] = $new_post_id;
-
-					// Get featured image upload_id - string, not int!
-					$featured_upload_id = $post['fields']['featured'] ?? null;
-					$featured_attachment_id = null;
-					$gallery_attachment_ids = [];
-
-					// Process all images for this post
-					foreach ($post['images'] as $img) {
-						$upload_id = $img['upload_id'];
-						$used_upload_ids[] = $upload_id;
-
-						if (isset($all_uploaded_images[$upload_id])) {
-							$attachment_id = $all_uploaded_images[$upload_id]['attachment_id'];
-
-							if ($upload_id === $featured_upload_id) {
-								$featured_attachment_id = $attachment_id;
-							} else {
-								$gallery_attachment_ids[] = $attachment_id;
-							}
-						}
-					}
-
-					// Set featured image
-					if ($featured_attachment_id) {
-						set_post_thumbnail($new_post_id, $featured_attachment_id);
-					} elseif (!empty($gallery_attachment_ids)) {
-						set_post_thumbnail($new_post_id, $gallery_attachment_ids[0]);
-						array_shift($gallery_attachment_ids);
-					}
-
-					// Set gallery images
-					if (!empty($gallery_attachment_ids)) {
-						$meta = Meta::forPost($new_post_id);
-						$fields = jvbGetFields($content, 'post');
-
-						foreach ($fields as $name => $config) {
-							if ($config['type'] === 'gallery') {
-								$meta->set($name, implode(',', $gallery_attachment_ids));
-								break;
-							}
-						}
-					}
-				}
-			}
-
-			return [
-				'success' => true,
-				'result' => [
-					'created_posts' => $created_posts,
-					'total_posts' => count($created_posts),
-					'used_images' => count($used_upload_ids),
-					'message' => "Created " . count($created_posts) . " post(s) from uploads"
-				]
-			];
-
-		} catch (Exception $e) {
-			JVB()->error()->log(
-				'[UploadRoutes]:processUploadGroups',
-				$e->getMessage(),
-				[
-					'operation_id' => $operation->id,
-					'user_id' => $operation->user_id
-				]
-			);
-
-			return [
-				'success' => false,
-				'result' => $e->getMessage()
-			];
-		}
-	}
-
-	protected function processTimelineUploads(array $data, array $uploads, array $uploadMap, object $operation):array
-	{
-		try {
-			$user = (int)$data['user'];
-			$created_posts = [];
-			$used_upload_ids = [];
-
-			$content = jvbCheckBase($data['content']);
-
-			$config = Features::getConfig($content);
-
-			$defaultTitle = 'New '.$config['singular']. ' ';
-			foreach ($data['posts'] as $index=> $post) {
-				$title = !empty($post['fields']['post_title'])
-					? sanitize_text_field($post['fields']['post_title'])
-					: $defaultTitle.($index + 1);
-				$excerpt = !empty($post['fields']['post_excerpt'])
-					? sanitize_textarea_field($post['fields']['post_excerpt'])
-					: '';
-
-				$args =[
-					'post_type'		=> $content,
-					'post_author'	=> $user,
-					'post_status'	=> 'draft',
-					'post_title'	=> $title,
-					'post_excerpt'	=> $excerpt
-				];
-				$parent = wp_insert_post($args);
-
-				if ($parent && !is_wp_error($parent)) {
-					//Get the attachment IDs first
-					$childPosts = [];
-					$featured = $post['fields']['featured']??null;
-					$featuredID = null;
-					foreach ($post['images'] as $key => $img) {
-						$upload_id = $img['upload_id'];
-						$used_upload_ids[] = $upload_id;
-
-						if (isset($uploadMap[$upload_id])) {
-							$attachment_id = (int)$uploadMap[$upload_id]['attachment_id'];
-							if ($upload_id === $featured) {
-								$featuredID = $attachment_id;
-							} else {
-								$childPosts[] = $attachment_id;
-							}
-						}
-					}
-					// Set the featured image for the parent
-					if ($featuredID) {
-						set_post_thumbnail($parent, $featuredID);
-					} elseif (!empty($childPosts)) {
-						//use first image if no set featured
-						set_post_thumbnail($parent, (int)$childPosts[0]);
-						array_shift($childPosts);
-					}
-
-					//Create Child Posts
-					if (!empty($childPosts)) {
-						$args['post_parent'] = $parent;
-						$created_posts[$parent] = [];
-						foreach ($childPosts as $i => $imgID) {
-							$treatment = $i + 1;
-							$childTitle = $title.' - Treatment '.$treatment;
-							$childDesc = '';
-							$args['post_title'] = $childTitle;
-							$args['post_excerpt'] = $childDesc;
-							$child = wp_insert_post($args);
-							if ($child && !is_wp_error($child)) {
-								$created_posts[$parent][] = $child;
-								set_post_thumbnail($child, $imgID);
-							}
-						}
-					}
-				}
-			}
-			return [
-				'success'	=> true,
-				'result'	=> [
-					'created_posts'	=> $created_posts,
-					'used_images'	=> $used_upload_ids
-				]
-			];
-		} catch (Exception $e) {
-			JVB()->error()->log(
-				'[UploadRoutes]:processTimelineUploads',
-				$e->getMessage(),
-				[
-					'operation_id' => $operation->id,
-					'user_id' => $operation->user_id
-				]
-			);
-
-			return [
-				'success' => false,
-				'result' => $e->getMessage()
-			];
-		}
-	}
-
-	protected function cleanupUnusedImages(array $unused_images): array
-	{
-		$cleaned_count = 0;
-		$errors = [];
-
-		foreach ($unused_images as $upload_id => $image_data) {
-			try {
-				$attachment_id = $image_data['attachment_id'];
-
-				// Verify this attachment exists and wasn't already deleted
-				if (get_post($attachment_id)) {
-					// Delete the attachment and its files
-					$deleted = wp_delete_attachment($attachment_id, true);
-
-					if ($deleted) {
-						$cleaned_count++;
-					} else {
-						$errors[] = "Failed to delete attachment {$attachment_id} for upload {$upload_id}";
-					}
-				} else {
-					// Attachment already doesn't exist, count as cleaned
-					$cleaned_count++;
-				}
-
-			} catch (Exception $e) {
-				$errors[] = "Error cleaning up upload {$upload_id}: " . $e->getMessage();
-			}
-		}
-
-		return [
-			'cleaned_count' => $cleaned_count,
-			'errors' => $errors
-		];
-	}
-
-	function getAttachmentID(array $image, array $storedResults): int|false
-	{
-		foreach ($storedResults as $operationID => $uploads) {
-			foreach ($uploads as $upload) {
-				// FIX: Handle both case variations
-				$stored_upload_id = $upload['upload_id'];
-				$search_upload_id = $image['upload_id'];
-
-				if ($stored_upload_id === $search_upload_id) {
-					return (int)$upload['attachment_id'];
-				}
-			}
-		}
-		return false;
-	}
-
-	function extractFeaturedItem(array &$items, string $meta_key = 'featured'): array
-	{
-		// Handle empty array
-		if (empty($items)) {
-			return [
-				'featured' => null,
-				'remaining' => []
-			];
-		}
-
-		$featured_index = null;
-
-		// First pass: look for featured item
-		foreach ($items as $index => $item) {
-			if (isset($item['meta'][$meta_key])) {
-				$featured_index = $index;
-				break;
-			}
-		}
-
-		// If no featured item found, use first item (index 0)
-		if ($featured_index === null) {
-			$featured_index = 0;
-		}
-
-		// Extract the featured/first item
-		$featured = $items[$featured_index];
-
-		// Remove the item from the original array
-		unset($items[$featured_index]);
-
-		// Re-index the array to maintain sequential indices
-		$remaining = array_values($items);
-
-		return [
-			'featured' => $featured,
-			'remaining' => $remaining
-		];
-	}
-
-	protected function mapUploadIdsToAttachments(array $uploadIds, array $uploadedImages): array
-	{
-		$mappedImages = [];
-
-		foreach ($uploadIds as $uploadId) {
-			$imageData = $this->findImageByUploadId($uploadId, $uploadedImages);
-			if ($imageData) {
-				$mappedImages[] = $imageData;
-			}
-		}
-
-		return $mappedImages;
-	}
-
-	protected function findImageByUploadId(string $uploadId, array $uploadedImages): ?array
-	{
-		// Handle both flat array and grouped results
-		foreach ($uploadedImages as $image) {
-			if (is_array($image)) {
-				// If it's a grouped result, check each image in the group
-				if (isset($image[0]) && is_array($image[0])) {
-					foreach ($image as $groupImage) {
-						if (isset($groupImage['upload_id']) && $groupImage['upload_id'] === $uploadId) {
-							return $groupImage;
-						}
-					}
-				} else {
-					// Single image result
-					if (isset($image['upload_id']) && $image['upload_id'] === $uploadId) {
-						return $image;
-					}
-				}
-			}
-		}
-
-		return null;
-	}
-
-	protected function determineFeaturedImage(array $group, array $groupImages): ?int
-	{
-		// Check if user has starred a specific image
-		if (!empty($group['featured_upload_id'])) {
-			foreach ($groupImages as $image) {
-				if (isset($image['upload_id']) && $image['upload_id'] === $group['featured_upload_id']) {
-					return $image['attachment_id'];
-				}
-			}
-		}
-
-		// Default to first image
-		return !empty($groupImages) ? $groupImages[0]['attachment_id'] : null;
-	}
-
-	protected function sanitizeGroupMetadata(array $metadata): array
-	{
-		$sanitized = [];
-
-		foreach ($metadata as $key => $value) {
-			$sanitizedKey = sanitize_key($key);
-
-			if (is_string($value)) {
-				$sanitized[$sanitizedKey] = sanitize_text_field($value);
-			} elseif (is_array($value)) {
-				$sanitized[$sanitizedKey] = array_map('sanitize_text_field', $value);
-			} else {
-				$sanitized[$sanitizedKey] = $value;
-			}
-		}
-
-		return $sanitized;
-	}
-
-	protected function generatePostTitle(string $content, int $userId): string
-	{
-		$username = get_user_meta($userId, 'first_name', true) ?: get_user_meta($userId, 'display_name', true);
-		$link = get_user_meta($userId, BASE.'link', true);
-		$city = function_exists('jvbArtistCity') ? jvbArtistCity($link) : '';
-
-		$title = ucfirst($content);
-		if ($username) {
-			$title .= ' by ' . $username;
-		}
-		if ($city) {
-			$title .= ' from ' . $city;
-		}
-
-		return $title;
-	}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-	/**
-	 * Determine how to save uploaded files based on configuration
-	 */
-	protected function handleUploadDestination(array $data, array $results): void
-	{
-		// Determine destination from config
-		$destination = $data['destination'] ?? 'meta';
-
-		switch ($destination) {
-			case 'meta':
-				// Save to post/term/user meta
-				$this->saveToMeta($data, $results);
-				break;
-
-			case 'post':
-				// Create individual posts for each file
-				$this->createIndividualPosts($data, $results);
-				break;
-
-			case 'post_group':
-				// Create posts with grouped files
-				$this->createGroupedPosts($data, $results);
-				break;
-
-			default:
-				// No destination specified - files processed but not attached
-				break;
-		}
-	}
-
-	/**
-	 * Infer destination from existing data (backward compatibility)
-	 */
-	protected function inferDestination(array $data): string
-	{
-		// If field_name exists → saving to meta
-		if (!empty($data['field_name'])) {
-			return 'meta';
-		}
-
-		// If post_type exists without field_name → creating posts
-		if (!empty($data['content'])) {
-			return 'post';
-		}
-
-		// No destination
-		return 'none';
-	}
-
-	private function getMetaManager(array $data): ?Meta
-	{
-		if (!empty($data['post_id'])) {
-			return Meta::forPost($data['post_id']);
-		}
-		if (!empty($data['term_id'])) {
-			return Meta::forTerm($data['term_id']);
-		}
-		if (!empty($data['user'])) {
-			$link = (int)get_user_meta($data['user'], BASE . 'link', true);
-			if ($link) {
-				return Meta::forPost($link);
-			}
-		}
-		return null;
-	}
-	/**
-	 * Save attachment IDs to meta field
-	 */
-	private function saveToMeta(array $data, array $results): void
-	{
-		if (empty($data['field_name'])) {
-			return;
-		}
-
-		$attachmentIds = array_column($results, 'attachment_id');
-		$meta = $this->getMetaManager($data);
-		if (!$meta) {
-			return;
-		}
-
-		$fieldType = $data['field_type'] ?? 'single';
-
-		if ($fieldType === 'single') {
-			// Single field: replace with latest upload
-			$meta->set($data['field_name'], end($attachmentIds));
-		} else {
-			// Multi field: merge with existing
-			$existing = $meta->get($data['field_name']);
-			$existingIds = !empty($existing) ? explode(',', $existing) : [];
-			$allIds = array_unique(array_merge($existingIds, $attachmentIds));
-			$meta->set($data['field_name'], implode(',', $allIds));
-		}
-	}
-
-	/**
-	 * Create individual posts from uploads
-	 */
-	protected function createIndividualPosts(array $data, array $results): array
-	{
-		if (empty($data['content'])) {
-			return [];
-		}
-
-		$created_posts = [];
-
-		foreach ($results as $result) {
-			$attachment_id = $result['attachment_id'];
-			$attachment = get_post($attachment_id);
-
-			// Create post
-			$post_data = [
-				'post_type' => jvbCheckBase($data['content']),
-				'post_title' => $attachment->post_title,
-				'post_status' => 'draft',
-				'post_author' => $data['user'] ?? get_current_user_id(),
-			];
-
-			$post_id = wp_insert_post($post_data);
-
-			if (!is_wp_error($post_id)) {
-				// Set as featured image or attach to gallery
-				$this->attachFileToPost($post_id, $attachment_id, $data);
-
-				$created_posts[] = [
-					'post_id' => $post_id,
-					'attachment_id' => $attachment_id,
-				];
-			}
-		}
-
-		return $created_posts;
-	}
-
-	/**
-	 * Create posts with grouped uploads
-	 */
-	protected function createGroupedPosts(array $data, array $results): array
-	{
-		if (empty($data['content'])) {
-			return [];
-		}
-
-		$id_map = [];
-		foreach ($results as $result) {
-			if (isset($result['upload_id'], $result['attachment_id'])) {
-				$id_map[$result['upload_id']] = $result['attachment_id'];
-			}
-		}
-
-		// Groups come from frontend as metadata
-		$groups = $data['groups'] ?? [array_column($results, 'attachment_id')];
-		$created_posts = [];
-
-		foreach ($groups as $group_index => $group_upload_ids) {
-			$group_attachment_ids = array_filter(
-				array_map(fn($uid) => $id_map[$uid] ?? null, $group_upload_ids)
-			);
-
-			if (empty($group_attachment_ids)) continue;
-			// Create post for this group
-			$first_attachment = get_post($group_attachment_ids[0]);
-
-			$post_data = [
-				'post_type' => jvbCheckBase($data['content']),
-				'post_title' => $data['group_titles'][$group_index] ?? $first_attachment->post_title,
-				'post_status' => $data['post_status'] ?? 'draft',
-				'post_author' => $data['user'] ?? get_current_user_id(),
-			];
-
-			$post_id = wp_insert_post($post_data);
-
-			if (!is_wp_error($post_id)) {
-				// Attach all files in group
-				foreach ($group_attachment_ids as $index => $attachment_id) {
-					if ($index === 0) {
-						// First is featured
-						set_post_thumbnail($post_id, $attachment_id);
-					} else {
-						// Others go to gallery
-						$meta = Meta::forPost($post_id);
-						$existing = $meta->get('gallery');
-						$existing_ids = !empty($existing) ? explode(',', $existing) : [];
-						$existing_ids[] = $attachment_id;
-						$meta->set('gallery', implode(',', $existing_ids));
-					}
-				}
-
-				$created_posts[] = [
-					'post_id' => $post_id,
-					'attachment_ids' => $group_attachment_ids,
-				];
-			}
-		}
-
-		return $created_posts;
-	}
-
-	/**
-	 * Attach file to post based on file type
-	 */
-	protected function attachFileToPost(int $post_id, int $attachment_id, array $data): void
-	{
-		$attachment = get_post($attachment_id);
-		$mime_type = $attachment->post_mime_type;
-
-		// Determine file type
-		if (str_starts_with($mime_type, 'image/')) {
-			// Set as featured image
-			set_post_thumbnail($post_id, $attachment_id);
-		} elseif (str_starts_with($mime_type, 'video/')) {
-			// Save to video field
-			$meta = Meta::forPost($post_id);
-			$meta->set('video', $attachment_id);
-		} else {
-			// Documents - save to documents field
-			$meta = Meta::forPost($post_id);
-			$existing = $meta->get('documents');
-			$existing_ids = !empty($existing) ? explode(',', $existing) : [];
-			$existing_ids[] = $attachment_id;
-			$meta->set('documents', implode(',', $existing_ids));
-		}
-	}
 }

--
Gitblit v1.10.0