<?php
|
namespace JVBase\managers;
|
|
use JVBase\meta\MetaManager;
|
use WP_Term;
|
use DateTime;
|
use DateMalformedStringException;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
/**
|
* 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 = 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 $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, ' ');
|
$title_format = (strlen($title_format) <= $this->character_limits['title']) ? $title_format : "{$title} | {$city}'s Best {$artist_type}";
|
return $title_format;
|
}
|
|
/**
|
* 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($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: ',
|
};
|
return "{$term->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 = "{$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
|
{
|
return "Edmonton's Best {$term->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
|
$description = "{$term->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;
|
}
|
}
|
|
$description .= " Find Edmonton artists specializing in {$term->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
|
{
|
return "Edmonton's Best {$term->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
|
$description = "Explore {$term->name} tattoos";
|
|
|
$description .= ", a popular motif in Edmonton's tattoo scene.";
|
|
|
$description .= $similar_text;
|
|
$description .= " Find artists specializing in {$term->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
|
{
|
return "{$term->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
|
$description = "Discover {$term->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();
|