<?php
|
namespace JVBase\managers;
|
|
use JVBase\meta\MetaManager;
|
use WP_Query;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
/**
|
* 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 = new MetaManager($post_id, 'post');
|
$metaValues = $meta->getAll();
|
|
$permalink = get_permalink($post_id);
|
|
// Get artist data
|
$name = get_the_title($post_id);
|
$first_name = $meta->getValue('first_name');
|
$bio = $meta->getValue('bio');
|
$short_bio = $meta->getValue('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 = new MetaManager($term_id, 'term');
|
$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' => $term->name,
|
'description' => $meta->getValue('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 = new MetaManager($term_id, 'term');
|
$term = get_term($term_id, BASE.'style');
|
$permalink = get_term_link($term_id, BASE.'style');
|
|
$schema = array_merge(
|
[
|
'@type' => 'CreativeWork',
|
'@id' => $permalink . '#style',
|
'name' => $term->name,
|
'description' => $meta->getValue('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 = new MetaManager($term_id, 'term');
|
$term = get_term($term_id, BASE.'theme');
|
$permalink = get_term_link($term_id, BASE.'theme');
|
|
$schema = [
|
'@type' => 'CreativeWork',
|
'@id' => $permalink . '#theme',
|
'name' => $term->name,
|
'description' => $meta->getValue('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 = new MetaManager($post_id, 'post');
|
$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
|
$schema = [
|
'@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'
|
]
|
],
|
];
|
|
return $schema;
|
}
|
|
/**
|
* 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'
|
]
|
];
|
}
|
}
|