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 = new MetaManager($post_id, 'post'); $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 = new MetaManager($post_id, 'post'); $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 = new MetaManager($term->term_id, 'term'); 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 = new MetaManager($term->term_id, 'term'); 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 * @param MetaManager $meta The meta manager * @return string The optimized title */ protected function getArtistTitle(string $title, MetaManager $meta):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 MetaManager $meta The meta manager * @return string The optimized description */ protected function getArtistDescription(int $post_id, MetaManager $meta):string { $bio = $meta->getValue('short_bio'); if ($bio !== '') { return $bio; } $first_name = $meta->getValue('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->getValue('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->getValue('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 MetaManager $meta The meta manager * @return string The optimized description */ protected function getPartnerDescription(int $post_id, MetaManager$meta):string { $short_bio = $meta->getValue('short_bio'); if ($short_bio !== '') { return $short_bio; } $established = $meta->getValue('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 = new MetaManager($post_id, 'post'); // Get event type if available $event_type = $meta->getValue('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->getValue('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 = new MetaManager($post_id, 'post'); $title = get_the_title($post_id); // Get event type if available $event_type = $meta->getValue('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->getValue('date_start'); $date_format = ''; if ($date_start) { $date = new DateTime($date_start); $date_format = $date->format('F j, Y'); } // Get location information $location = $meta->getValue('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->getValue('is_free'); if ($is_free) { $description .= ". Free admission"; } else { $cost = $meta->getValue('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 MetaManager $meta The meta manager * @return string The optimized title */ protected function getShopTitle(WP_Term $term, MetaManager $meta):string { $city_id = $meta->getValue('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 MetaManager $meta The meta manager * @return string The optimized description */ protected function getShopDescription(WP_Term $term, MetaManager $meta):string { $short_bio = $meta->getValue('short_bio'); if ($short_bio !== '') { return $short_bio; } $established = $meta->getValue('established'); // Get city $city_id = $meta->getValue('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 MetaManager $meta The meta manager * @return string The optimized description */ protected function getStyleDescription(WP_Term $term, MetaManager $meta):string { $tagline = $meta->getValue('tagline'); if (!$tagline !== '') { return $tagline; } $characteristics = $meta->getValue('characteristics'); $alternate_names = $meta->getValue('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 MetaManager $meta The meta manager * @return string The optimized description */ protected function getThemeDescription(WP_Term $term, MetaManager $meta):string { $description_meta = $meta->getValue('description'); if ($description_meta !== '') { return $description_meta; } // Get similar themes if available $similar = $meta->getValue('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();