<?php
|
namespace JVBase\registrar\config\seo;
|
|
use JVBase\base\SchemaHelper;
|
use JVBase\managers\Cache;
|
use JVBase\managers\SEO\render;
|
use JVBase\meta\Meta;
|
use JVBase\registrar\Registrar;
|
use WP_Query;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class Schema {
|
//One of the defined classes in the JVBase\managers\SEO\render namespace;
|
protected mixed $schema;
|
protected string $slug;
|
protected Meta $meta;
|
protected Cache $cache;
|
protected Cache $referenceCache;
|
protected Cache $archiveCache;
|
|
protected array $properties = [];
|
protected array $referenceProperties = [];
|
protected array $defaultReference = [
|
'name' => '{{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->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();
|
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 [];
|
}
|
jvbDump($this->slug);
|
jvbDump($type);
|
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 = $config['name'];
|
} 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();
|
}
|
}
|