'{{post_title}}', 'url' => '{{post_permalink}}', 'description' => '{{post_excerpt}}', ]; protected array $defaultSchema = [ 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork', 'title' => '{{post_title}}', 'url' => '{{post_permalink}}', 'description' => '{{post_excerpt}}', ]; protected array $defaultArchive = [ 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', 'title' => '{{registrar.plural}}' ]; public function __construct(string $slug, string $type) { if (!class_exists($type)) { error_log('Could not find schema class: '.$type); return; } $this->schema = new $type(); $this->slug = $slug; $registrar = Registrar::getInstance($this->slug); $this->cache = Cache::for('schema'); $this->referenceCache = Cache::for('schemaReference'); $this->archiveCache = Cache::for('schemaArchive'); if ($registrar) { $this->cache->connect($registrar->getType()); $this->referenceCache->connect($registrar->getType()); $this->archiveCache->connect($registrar->getType()); switch ($registrar->getType()) { case 'term': $this->defaultSchema['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage'; break; case 'user': $this->defaultSchema['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ProfilePage'; break; } $this->defaultArchive['description'] = '{{registrar.'.$slug.'.description}}'; } $this->extras = apply_filters(BASE.ucfirst($slug).'Extras', []); $this->initFilters(); $this->registerHooks(); } public function initFilters():void { $referenceProperties = apply_filters(BASE.ucFirst($this->slug).'ReferenceProperties', $this->defaultReference); foreach ($referenceProperties as $property => $value) { $this->defineReference($property, $value); } $registrar = Registrar::getInstance($this->slug); $this->defaultArchive = [ 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', 'name' => $registrar->getPlural(), 'description' => $registrar->getDescription() ]; } public function registerHooks(): void { add_action('wp_head', [$this, 'outputSchema'], 1); add_filter('the_seo_framework_schema_graph_data', [$this, 'filterTSFSchema'], 10, 2); add_filter('the_seo_framework_title_from_custom_field', [$this, 'filterTSFOGTitle'], 10, 2); $this->maybeExcludeSingles(); } protected function maybeExcludeSingles(): void { $exclude = $this->cache->remember( 'excludeSingles', function () { $exclude = []; $registrar = Registrar::getInstance($this->slug); if ($registrar && $registrar->hasFeature('hide_single')) { $exclude = $this->excludeSingle(); } if ($registrar && $registrar->hasFeature('is_timeline')) { $exclude = array_merge($exclude, $this->excludeTimeline()); } return $exclude; } ); if (!empty($exclude)) { add_filter('the_seo_framework_sitemap_exclude_ids', $exclude); } } protected function excludeSingle():array { return get_posts([ 'post_type' => jvbCheckBase($this->slug), 'posts_per_page'=> -1, 'fields' => 'ids', 'post_status' => 'publish', ]); } protected function excludeTimeline():array { return get_posts([ 'post_type' => jvbCheckBase($this->slug), 'posts_per_page'=> -1, 'fields' => 'ids', 'post_status' => 'publish', 'post_parent__not_in' => [0], // Only get posts with a parent ]); } public function filterTSFSchema(array $graph, ?array $args):array { $based = jvbCheckBase($this->slug); if (is_front_page() || is_singular($based) || is_post_type_archive($based) || is_tax($based)) { return []; } // if (jvbTSFDoIt($this->slug, $args)){ // return []; // } return $graph; } public function outputSchema():void { $registrar = Registrar::getInstance($this->slug); if (is_singular()) { $this->outputSingularSchema(); } elseif (is_post_type_archive(jvbCheckBase($this->slug) || is_tax(jvbCheckBase($this->slug)))) { $this->outputArchiveSchema(); } if ($registrar && $registrar->hasFeature('is_content') && is_single(get_option(BASE.$this->slug.'_archive'))) { $this->outputContentTaxArchiveSchema(); } } public function outputSingularSchema():array { $ID = get_the_ID(); if (JVB_TESTING){ $this->cache->flush(); } $registrar = Registrar::getInstance($this->slug); return $this->cache->remember( $ID, function () use ($ID, $registrar) { $meta = Meta::forPost($ID); if ($registrar->hasFeature('is_faq')) { return $this->outputQASchema($ID, $meta); } if ($registrar->hasFeature('is_timeline')) { return $this->outputTimelineSchema($ID, $meta); } $config = $this->getConfig(); $class = JVB()->schemaHelper()::classFromConfig($config, $meta); $class->setAuthor(JVB()->seo()->getCreator(true)); $class = apply_filters('jvb_single_'.$this->slug.'_schema_output', $class, $ID); return $class->outputSchema(); } ); } public function outputContentTaxArchiveSchema():array { $ID = get_the_ID(); if (JVB_TESTING) { $this->cache->flush(); } return $this->cache->remember( $ID, function() use ($ID) { $action = BASE.ucfirst($this->slug).'Schema'; $config = JVB()->schemaHelper()::schema($action); if (!array_key_exists('type', $config)) { $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage'; } if (!class_exists($config['type'])) { error_log('No class found for archive schema output: '.$config['type']); return []; } $class = JVB()->schemaHelper()::classFromConfig($config); $class->setIsPartOf(get_home_url().'/#website'); $itemList = new render\Thing\Intangible\ItemList\ItemList(); $items = get_terms([ 'taxonomy' => jvbCheckBase($this->slug), // 'hide_empty' => true, 'fields' => 'ids', ]); $pos = 1; $itemListItems = []; foreach ($items as $ID) { $item = $this->outputReferenceSchema($ID, 'term',false); $listItem = new render\Thing\Intangible\ListItem(); $listItem->setPosition($pos); $listItem->setItem($item); $itemListItems[] = $listItem; $pos++; } wp_reset_postdata(); $itemList->setItemListElement($itemListItems); $class->setMainEntity($itemList); return $class->outputSchema(); } ); } public function outputArchiveSchema():array { if (JVB_TESTING){ $this->archiveCache->flush(); } return $this->archiveCache->remember( $this->slug, function() { $action = BASE.ucfirst($this->slug).'Archive'; $config = JVB()->schemaHelper()->archive($this->slug); if (!array_key_exists('type', $config)) { $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage'; } if (!class_exists($config['type'])) { error_log('No class found for archive schema output: '.$config['type']); return []; } $registrar = Registrar::getInstance($this->slug); if ($registrar->hasFeature('is_glossary')) { return $this->outputGlossarySchema(); } else if ($registrar->hasFeature('is_faq')) { return $this->outputFAQSchema(); } if ($registrar->hasFeature('is_timeline')) { return $this->outputTimelineArchiveSchema(); } $obj = get_queried_object(); $meta = (property_exists($obj, 'taxonomy')) ? Meta::forTerm($obj->term_id) : null; $class = JVB()->schemaHelper()::classFromConfig($config, $meta); $class->setIsPartOf(get_home_url().'/#website'); $itemList = new render\Thing\Intangible\ItemList\ItemList(); $items = new WP_Query([ 'post_type' => jvbCheckBase($this->slug), 'posts_per_page'=> 25, 'post_status' => 'publish', 'fields' => 'ids' ]); $pos = 1; $itemListItems = []; foreach ($items->posts as $ID) { $item = $this->outputReferenceSchema($ID, 'post',false); $listItem = new render\Thing\Intangible\ListItem(); $listItem->setId('listitem-'.$ID); $listItem->setPosition($pos); $listItem->setItem($item); $itemListItems[] = $listItem; $pos++; } wp_reset_postdata(); $itemList->setItemListElement($itemListItems); $class->setMainEntity($itemList); $schema = $class->outputSchema(); if (JVB_TESTING) { // error_log('Generated archive schema: '.print_r($schema, true)); } return $schema; } ); } public function outputReferenceSchema(int $ID, string $type, bool $outputSchema = true):mixed { if (JVB_TESTING){ $this->referenceCache->flush(); } $cached = $this->referenceCache->remember( $ID, function () use ($ID, $type) { switch ($type) { case 'post': $meta = Meta::forPost($ID); break; case 'term': $meta = Meta::forTerm($ID); break; case 'user': $meta = Meta::forUser($ID); break; default: error_log('Invalid type used for reference: '.print_r($type, true)); $meta = null; } $config = $this->getConfig(); $class = JVB()->schemaHelper()::classFromConfig($config, $meta); $class->delete('about'); switch ($type) { case 'post': $class->setId(get_the_permalink($ID).'#'.$class->getTypeName()); break; case 'term': $class->setId(get_term_link($ID).'#'.$class->getTypeName()); break; } return $class; } ); return $outputSchema ? $cached->outputSchema() : $cached; } public function getConfig(string $type = 'schema'):array { if (!in_array(strtolower($type), ['schema', 'meta', 'archive', 'reference'])) { error_log('[SEO]Schema::getConfig Invalid type: '.$type); return []; } return JVB()->schemaHelper()::getConfig($this->slug, $type); } public function resolveMeta(array $config, Meta $meta):array { foreach ($config as $property => $value) { if (is_array($value)) { $config[$property] = $this->resolveMeta($value, $meta); if ($property === 'additionalProperty' && (!array_key_exists('value', $config[$property]) || empty($config[$property]['value']))) { unset($config[$property]); } } if (is_string($value) && str_contains($value, '{{')) { $value = Resolver::resolve($value, $meta); if (empty($value)){ unset($config[$property]); } else { $config[$property] = $value; } } } return $config; } public function define(string $property, string $value):void { $class = $this->getConfig('schema')['type']; if (!class_exists($class)) { error_log('[SEO]Schema::defineReference Class not found: '.$class); return; } if ($property === 'type') { $this->properties[$property] = $value; return; } if (!property_exists($class, $property)){ error_log('Attempted to add non-existent property '.$property.' with value: '.print_r($value, true)); return; } $this->properties[$property] = $value; } public function defineReference(string $property, string $value):void { $config = $this->getConfig(); if (!array_key_exists('type', $config)) { $config['type'] = $this->defaultSchema['type']; update_option(BASE.ucfirst($this->slug).'Schema', $config); } $class = $config['type']; if (!class_exists($class)) { error_log('[SEO]Schema::defineReference Class not found: '.$class); return; } if ($property === 'type') { $this->referenceProperties[$property] = $value; return; } $class = new $class(); if (!property_exists($class, $property)){ error_log('Attempted to add non-existent property '.$property.' with value: '.print_r($value, true)); return; } $this->referenceProperties[$property] = $value; } public function setAllProperties(array $properties):void { foreach ($properties as $property => $value){ $this->define($property, $value); } } public function setAllReferenceProperties(array $properties):void { foreach ($properties as $property => $value){ $this->defineReference($property, $value); } } public function filterTSFOGTitle(string $title, ?array $args):string{ $based = jvbCheckBase($this->slug); if (is_singular($based)){ $config = $this->getConfig('meta'); $meta = Meta::forPost(get_the_ID()); $title = Resolver::resolve($config['name']??$config['title']??'', $meta); } elseif (is_post_type_archive($based) ) { $config = $this->getConfig('archive'); $title = Resolver::resolve($config['name'], null); } elseif (is_tax($based)) { $config = $this->getConfig('archive'); $meta = Meta::forTerm(get_queried_object_id()); $title = Resolver::resolve($config['name'], $meta); } else { error_log('[Schema]::filterTSFOGTitle Unmatched condition: '.$this->slug); } return $title; } public function outputQASchema(int $ID, Meta $meta):array { $registrar = Registrar::getInstance($this->slug); global $wp; $current = get_home_url(null, $wp->request).'/'; $config = $this->getConfig(); $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\QAPage'; $page = SchemaHelper::classFromConfig($config, $meta); $post = get_queried_object(); $question = [ 'id' => $current.'#question-'.$post->post_name, 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Question', 'name' => $meta->get('post_title'), 'acceptedAnswer' => [ 'id' => $current.'#answer-'.$post->post_name, 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer', 'text' => wp_strip_all_tags(str_replace("\n", '', $meta->get('post_content'))), ], ]; $question = SchemaHelper::classFromConfig($question); $page->setMainEntity($question); $page->setAuthor(JVB()->seo()->getCreator(true)); return $page->outputSchema(); } public function outputTimelineSchema(int $ID, Meta $meta):array { $registrar = Registrar::getInstance($this->slug); global $wp; $current = get_home_url(null, $wp->request).'/'; $config = $this->getConfig(); $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\ImageGallery'; $this->resolveMeta($config, $meta); $page = SchemaHelper::classFromConfig($config, $meta); $post = get_queried_object(); $images = []; $children = new WP_Query([ 'post_type'=> jvbCheckBase($this->slug), 'post_status' => 'publish', 'post_parent' => $post->ID, ]); $children = $children->posts; array_unshift($children, $post); foreach ($children as $index => $child) { $meta = Meta::forPost($child->ID); $image = render\Thing\Thing::createImageFromID($meta->get('post_thumbnail')); $image->setId(($index === 0) ? '#before-treatment' : '#treatment-'.$index); $image->setPosition($index+1); $image->setName(($index === 0) ? 'Before Laser Tattoo Removal' : 'After '.$index.' Laser Tattoo Removal Treatments'); if (!empty ($meta->get('post_excerpt'))){ $image->setDescription($meta->get('post_excerpt')); } $images[] = $image; } $page->setImage($images); $page->setAuthor(JVB()->seo()->getCreator(true)); return $page->outputSchema(); } /********************************************* * Archive Presets *********************************************/ public function outputFAQSchema():array { $registrar = Registrar::getInstance($this->slug); global $wp; $current = get_home_url(null, $wp->request).'/'; $config = $this->getConfig('archive'); $page = [ 'id' => $current.'#'.$registrar->getSlug(), 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\FAQPage', 'name' => array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(), 'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(), 'url' => $current, ]; $page = SchemaHelper::classFromConfig($page); $args = [ 'post_type' => $registrar->getBased(), 'posts_per_page'=> -1, 'post_status' => 'publish', ]; $obj = get_queried_object(); if (property_exists($obj, 'taxonomy')) { $page->setName('FAQ on '.$obj->name); $args['post_type'] = array_map('jvbCheckBase', $registrar->registrar->for); $args['tax_query'] = [ [ 'taxonomy' => $obj->taxonomy, 'terms' => $obj->term_id, ] ]; } $questions = []; $posts = new WP_Query($args); foreach ($posts->posts as $post) { $meta = Meta::forPost($post->ID); $question = [ 'id' => $current.'#question-'.$post->post_name, 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Question', 'name' => $meta->get('post_title'), 'acceptedAnswer' => [ 'id' => $current.'#answer-'.$post->post_name, 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer', 'text' => $meta->get('post_excerpt'), ], 'url' => get_the_permalink($post->ID), ]; $questions[] = SchemaHelper::classFromConfig($question); } wp_reset_postdata(); $page->setMainEntity($questions); return $page->outputSchema(); } public function outputTimelineArchiveSchema():array { $registrar = Registrar::getInstance($this->slug); global $wp; $current = get_home_url(null, $wp->request).'/'; $config = $this->getConfig('archive'); $page = array_merge($config, [ 'id' => $current.'#'.$registrar->getSlug(), 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', 'name' => array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(), 'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(), 'url' => $current, ]); $page = SchemaHelper::classFromConfig($page); $parts = []; $timelines = new WP_Query([ 'post_type' => $registrar->getBased(), 'posts_per_page'=> 50, 'post_status' => 'publish', 'post_parent' => 0, ]); foreach ($timelines->posts as $post) { $item = $this->outputReferenceSchema($post->ID, 'post', false); $item->setId($current.'#'.$post->post_name); $item->setName($post->post_title); $item->setUrl(get_the_permalink($post->ID)); $parts[] = $item; } $page->setHasPart($parts); return $page->outputSchema(); } public function outputGlossarySchema():array { $registrar = Registrar::getInstance($this->slug); global $wp; $current = get_home_url(null, $wp->request).'/'; $config = $this->getConfig('archive'); $page = [ 'id' => $current.'#'.$registrar->getSlug(), 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', 'name' => array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(), 'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(), 'url' => $current, ]; $page = SchemaHelper::classFromConfig($page); //Defined Termset $termset = [ 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\DefinedTermSet', 'id' => $current.'#definedtermset', 'name' => $registrar->getPlural(), 'description' => $registrar->getDescription(), ]; $termset = SchemaHelper::classFromConfig($termset); $terms = new WP_Query([ 'post_type' => $registrar->getBased(), 'posts_per_page' => -1, 'post_status' => 'publish', ]); $outputTerms = []; foreach ($terms->posts as $post) { $item = $this->outputReferenceSchema($post->ID, 'post', false); $item->setId($current.'#'.$post->post_name); $item->setName($post->post_title); $outputTerms[] = $item; } $termset->setHasDefinedTerm($outputTerms); $page->setMainEntity($termset); return $page->outputSchema(); } public function extra():array { if (empty($this->extras)) { return []; } $out = []; foreach ($this->extras as $config) { $schema = SchemaHelper::classFromConfig($config); $output = $schema->outputSchema(); if (!empty($output)) { $out[] = $output; } } return $out; } }