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