From 48721c85ebcfa973ee81719d2467ca80e4253dc9 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 01 May 2026 17:30:03 +0000
Subject: [PATCH] =Edmonton Ink hard test begins! Real testing of the managers and reset routes will commence. So far, just ensuring our classes are all loaded correctly: Site() and its sub-classes Membership, Login, etc. Care should be taken to load conditionally on 'init', as we finish defining most settings by 'plugins_loaded' at priority 5

---
 inc/registrar/config/seo/Schema.php |  446 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 405 insertions(+), 41 deletions(-)

diff --git a/inc/registrar/config/seo/Schema.php b/inc/registrar/config/seo/Schema.php
index 91f03cb..0214fa5 100644
--- a/inc/registrar/config/seo/Schema.php
+++ b/inc/registrar/config/seo/Schema.php
@@ -1,6 +1,7 @@
 <?php
 namespace JVBase\registrar\config\seo;
 
+use JVBase\base\SchemaHelper;
 use JVBase\managers\Cache;
 use JVBase\managers\SEO\render;
 use JVBase\meta\Meta;
@@ -34,7 +35,10 @@
 		'description' => '{{post_excerpt}}',
 	];
 
-	protected array $defaultArchive = [];
+	protected array $defaultArchive = [
+		'type'	=> 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
+		'title' => '{{registrar.plural}}'
+	];
 
 	public function __construct(string $slug, string $type)
 	{
@@ -52,6 +56,16 @@
 			$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();
@@ -66,7 +80,7 @@
 
 		$registrar = Registrar::getInstance($this->slug);
 		$this->defaultArchive = [
-			'type'	=> 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage',
+			'type'	=> 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
 			'name'	=> $registrar->getPlural(),
 			'description' => $registrar->getDescription()
 		];
@@ -75,6 +89,8 @@
 	{
 		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
@@ -119,67 +135,142 @@
 		}
 	public function filterTSFSchema(array $graph, ?array $args):array
 	{
-		if (jvbTSFDoIt($this->slug, $args)){
+		$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();
-			$this->cache->flush();
+			if (JVB_TESTING){
+				$this->cache->flush();
+			}
+
+			$registrar = Registrar::getInstance($this->slug);
+
 			return $this->cache->remember(
 				$ID,
-				function () use ($ID) {
+				function () use ($ID, $registrar) {
 					$meta = Meta::forPost($ID);
-					$config = $this->getConfig();
-
-					$class = new $config['type']();
-					unset($config['type']);
-					foreach ($config as $property => $value){
-						$value = Resolver::resolveForSchema($property, $value, $config, $meta);
-						$method = 'set'.ucfirst($property);
-						$class->$method($value);
+					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));
-					$schema = $class->outputSchema();
-					error_log('Generated archive schema: '.print_r($schema, true));
-					return $schema;
+
+					$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
 		{
-			$this->archiveCache->flush();
+			if (JVB_TESTING){
+				$this->archiveCache->flush();
+			}
+
 			return $this->archiveCache->remember(
 				$this->slug,
 				function() {
 					$action = BASE.ucfirst($this->slug).'Archive';
-					$config = get_option($action, apply_filters($action, $this->defaultArchive));
-
+					$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 [];
 					}
 
-					$class = new $config['type'];
-					unset($config['type']);
-					foreach ($config as $property=>$value) {
-						$method = 'set'.ucfirst($property);
-						$class->$method($value);
+					$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([
@@ -191,8 +282,9 @@
 					$pos = 1;
 					$itemListItems = [];
 					foreach ($items->posts as $ID) {
-						$item = $this->outputReferenceSchema($ID, false);
+						$item = $this->outputReferenceSchema($ID, 'post',false);
 						$listItem = new render\Thing\Intangible\ListItem();
+						$listItem->setId('listitem-'.$ID);
 						$listItem->setPosition($pos);
 						$listItem->setItem($item);
 						$itemListItems[] = $listItem;
@@ -209,24 +301,43 @@
 			);
 		}
 
-	public function outputReferenceSchema(int $ID, bool $outputSchema = true):mixed
+	public function outputReferenceSchema(int $ID, string $type, bool $outputSchema = true):mixed
 	{
-		$this->referenceCache->flush();
+		if (JVB_TESTING){
+			$this->referenceCache->flush();
+		}
+
 		$cached = $this->referenceCache->remember(
 			$ID,
-			function () use ($ID) {
-				$meta = Meta::forPost($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 = new $config['type']();
-				$class->setId(get_the_permalink($ID).'/#'.$class->getTypeName());
-				foreach ($this->referenceProperties as $property => $value){
-					$value = Resolver::resolveForSchema($property, $value, $this->schema, $meta);
-					$method = 'set'.ucfirst($property);
-					$class->$method($value);
+				$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;
 				}
 
-				$schema = $class->outputSchema();
-				error_log('Generated archive schema: '.print_r($schema, true));
+
 				return $class;
 			}
 		);
@@ -240,9 +351,31 @@
 			error_log('[SEO]Schema::getConfig Invalid type: '.$type);
 			return [];
 		}
-		$action = BASE.ucfirst($this->slug).ucfirst($type);
-		$default = 'default'.ucfirst($type);
-		return get_option($action, apply_filters($action, $this->$default));
+		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
@@ -264,7 +397,12 @@
 	}
 	public function defineReference(string $property, string $value):void
 	{
-		$class = $this->getConfig('schema')['type'];
+		$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;
@@ -294,4 +432,230 @@
 			$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();
+	}
 }

--
Gitblit v1.10.0