From 83985b1f1534d70cca59edb01627638c890116cb Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 09 Feb 2026 01:36:09 +0000
Subject: [PATCH] =Schema Updates

---
 assets/js/concise/SchemaManager.js       |    9 ++-------
 inc/managers/SEO/SEOAdminPage.php        |    7 ++++++-
 inc/managers/SEO/SchemaBuilder.php       |   17 +++++------------
 inc/managers/SEO/SchemaOutputManager.php |   24 ++++++++++++------------
 assets/js/min/schema.min.js              |    2 +-
 5 files changed, 26 insertions(+), 33 deletions(-)

diff --git a/assets/js/concise/SchemaManager.js b/assets/js/concise/SchemaManager.js
index 4d2ae40..b4aba23 100644
--- a/assets/js/concise/SchemaManager.js
+++ b/assets/js/concise/SchemaManager.js
@@ -15,18 +15,13 @@
 
 	init() {
 		// Initialize FormController
-		if (window.jvbForm && !window.formController) {
-			this.formController = new window.jvbForm();
-			window.formController = this.formController;
-		} else if (window.formController) {
-			this.formController = window.formController;
-		}
+		this.formController = window.formController
 
 		// Initialize main Tabs (they're outside forms, so FormController won't handle them)
 		if (window.jvbTabs) {
 			const tabContainer = document.querySelector('.jvb-seo-admin');
 			if (tabContainer) {
-				this.tabsInstance = new window.jvbTabs(tabContainer);
+				this.tabsInstance = window.jvbTabs.registerTab(tabContainer);
 			}
 		}
 
diff --git a/assets/js/min/schema.min.js b/assets/js/min/schema.min.js
index 449035b..d2db817 100644
--- a/assets/js/min/schema.min.js
+++ b/assets/js/min/schema.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.formController=null,this.tabsInstance=null,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.init()}init(){if(window.jvbForm&&!window.formController?(this.formController=new window.jvbForm,window.formController=this.formController):window.formController&&(this.formController=window.formController),window.jvbTabs){const e=document.querySelector(".jvb-seo-admin");e&&(this.tabsInstance=new window.jvbTabs(e))}this.formController&&this.formController.subscribe(((e,t)=>{"form-submit"===e&&this.handleFormSubmit(t)})),this.queue&&this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&"seo"===t.endpoint&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initializeForms(),this.addPreservedFieldStyles()}initializeForms(){document.querySelectorAll('form[data-save="seo"]').forEach((e=>{this.formController&&this.formController.registerForm(e,{endpoint:"seo",autosave:!1,formStatus:!1}),this.initializeTypeSwitch(e);const t=e.querySelector('[data-action="reset"]');t&&t.addEventListener("click",(()=>this.handleReset(e)))}))}handleFormSubmit(e){const t=e.config.element.dataset.content,n=e.fullData,o={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"save",...n},popup:"Saving SEO configuration",title:`Saving ${t} settings`};this.queue.addToQueue(o)}async handleReset(e){const t=e.dataset.content;if(!confirm("Reset to default settings? This cannot be undone."))return;const n={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"reset"},popup:"Resetting configuration",title:`Resetting ${t} to defaults`};this.queue.addToQueue(n)}handleQueueSuccess(e,t){console.log("SEO save successful:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Configuration saved successfully"),"reset"===t.operation?.data?.action&&t.response?.schema&&this.reloadFormData(t.operation.data.context,t.response)}handleQueueFailure(e,t){console.error("SEO operation failed permanently:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(`Error: ${t.error_message||"Operation failed"}`)}reloadFormData(e,t){const n=document.querySelector(`form[data-content="${e}"]`);if(!n)return;const o=t.schema||{};Object.keys(o).forEach((e=>{const t=n.querySelector(`[name="${e}"]`);t&&("checkbox"===t.type?t.checked=!!o[e]:t.value=o[e]||"")})),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Form reset to defaults")}initializeTypeSwitch(e){const t=e.querySelector('select[name="type"]');t&&(t.addEventListener("change",(n=>{const o=e.dataset.currentType||t.dataset.initialValue,a=n.target.value;o!==a&&this.confirmTypeChange(e,t,o,a)})),t.dataset.initialValue=t.value,e.dataset.currentType=t.value)}confirmTypeChange(e,t,n,o){const a={},i=new FormData(e);for(let[e,t]of i.entries())"type"!==e&&t&&""!==t&&(a[e]=t);const r=window.getTemplate(`seo-${o}`);if(!r)return console.error("No template found for type:",o),void(t.value=n);const s=e=>e.split(":")[0],l=new Set(Object.keys(a).map(s)),c=r.querySelectorAll("[data-field]"),u=new Set(Array.from(c).map((e=>e.dataset.field)));if(0===u.size){const e=r.querySelectorAll("[name]");Array.from(e).forEach((e=>{u.add(s(e.getAttribute("name")))}))}const d=[...l].filter((e=>u.has(e))),h=[...l].filter((e=>!u.has(e)));let f=`Change schema type from ${n} to ${o}?\n\n`;d.length>0&&(f+=`✓ ${d.length} field value(s) will be preserved:\n`,f+=d.map((e=>`  • ${e}`)).join("\n"),f+="\n\n"),h.length>0&&(f+=`⚠ ${h.length} field value(s) will be lost:\n`,f+=h.map((e=>`  • ${e}`)).join("\n")),confirm(f)?this.handleTypeChange(e,t,o):(t.value=n,this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Type change cancelled"))}handleTypeChange(e,t,n){const o=e.dataset.currentType||t.dataset.initialValue,a=this.collectFormData(e),i=window.getTemplate(`seo-${n}`);if(!i)return void console.error("No template found for type:",n);const r=e.querySelector(".seo-"+o);if(r&&(r.parentNode.insertBefore(i,r),r.remove()),e.dataset.currentType=n,window.jvbPopulate){this.populate=window.jvbPopulate.populate,Object.keys(a).forEach((t=>{const n=e.querySelector(`[data-field="${t}"]`);if(n){const e=a[t];this.populate(n,t,e)}}));const t=`Schema type changed to ${n}.`;this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(t)}}collectFormData(e){const t={},n=new FormData(e);for(let[e,o]of n.entries())if("type"!==e&&"context"!==e)if(e.includes(":")){const n=e.split(":"),a=n[0],i=parseInt(n[1]),r=n[2];t[a]||(t[a]=[]),t[a][i]||(t[a][i]={}),t[a][i][r]=o}else t[e]=o;return t}getFieldType(e){return e.classList.contains("repeater")?"repeater":"text"}addPreservedFieldStyles(){const e=document.createElement("style");e.textContent="\n            .value-preserved {\n                background-color: #e7f5e7 !important;\n                transition: background-color 0.3s ease;\n            }\n        ",document.head.appendChild(e)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSchema=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.formController=null,this.tabsInstance=null,this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.init()}init(){if(this.formController=window.formController,window.jvbTabs){const e=document.querySelector(".jvb-seo-admin");e&&(this.tabsInstance=window.jvbTabs.registerTab(e))}this.formController&&this.formController.subscribe(((e,t)=>{"form-submit"===e&&this.handleFormSubmit(t)})),this.queue&&this.queue.subscribe(((e,t)=>{Object.hasOwn(t,"endpoint")&&"seo"===t.endpoint&&("operation-completed"===e?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&this.handleQueueFailure(e,t))})),this.initializeForms(),this.addPreservedFieldStyles()}initializeForms(){document.querySelectorAll('form[data-save="seo"]').forEach((e=>{this.formController&&this.formController.registerForm(e,{endpoint:"seo",autosave:!1,formStatus:!1}),this.initializeTypeSwitch(e);const t=e.querySelector('[data-action="reset"]');t&&t.addEventListener("click",(()=>this.handleReset(e)))}))}handleFormSubmit(e){const t=e.config.element.dataset.content,n=e.fullData,o={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"save",...n},popup:"Saving SEO configuration",title:`Saving ${t} settings`};this.queue.addToQueue(o)}async handleReset(e){const t=e.dataset.content;if(!confirm("Reset to default settings? This cannot be undone."))return;const n={endpoint:"seo",headers:{"X-WP-Nonce":window.auth.getNonce()},data:{context:t,action:"reset"},popup:"Resetting configuration",title:`Resetting ${t} to defaults`};this.queue.addToQueue(n)}handleQueueSuccess(e,t){console.log("SEO save successful:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Configuration saved successfully"),"reset"===t.operation?.data?.action&&t.response?.schema&&this.reloadFormData(t.operation.data.context,t.response)}handleQueueFailure(e,t){console.error("SEO operation failed permanently:",t),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(`Error: ${t.error_message||"Operation failed"}`)}reloadFormData(e,t){const n=document.querySelector(`form[data-content="${e}"]`);if(!n)return;const o=t.schema||{};Object.keys(o).forEach((e=>{const t=n.querySelector(`[name="${e}"]`);t&&("checkbox"===t.type?t.checked=!!o[e]:t.value=o[e]||"")})),this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Form reset to defaults")}initializeTypeSwitch(e){const t=e.querySelector('select[name="type"]');t&&(t.addEventListener("change",(n=>{const o=e.dataset.currentType||t.dataset.initialValue,a=n.target.value;o!==a&&this.confirmTypeChange(e,t,o,a)})),t.dataset.initialValue=t.value,e.dataset.currentType=t.value)}confirmTypeChange(e,t,n,o){const a={},s=new FormData(e);for(let[e,t]of s.entries())"type"!==e&&t&&""!==t&&(a[e]=t);const i=window.getTemplate(`seo-${o}`);if(!i)return console.error("No template found for type:",o),void(t.value=n);const r=e=>e.split(":")[0],c=new Set(Object.keys(a).map(r)),l=i.querySelectorAll("[data-field]"),u=new Set(Array.from(l).map((e=>e.dataset.field)));if(0===u.size){const e=i.querySelectorAll("[name]");Array.from(e).forEach((e=>{u.add(r(e.getAttribute("name")))}))}const d=[...c].filter((e=>u.has(e))),h=[...c].filter((e=>!u.has(e)));let p=`Change schema type from ${n} to ${o}?\n\n`;d.length>0&&(p+=`✓ ${d.length} field value(s) will be preserved:\n`,p+=d.map((e=>`  • ${e}`)).join("\n"),p+="\n\n"),h.length>0&&(p+=`⚠ ${h.length} field value(s) will be lost:\n`,p+=h.map((e=>`  • ${e}`)).join("\n")),confirm(p)?this.handleTypeChange(e,t,o):(t.value=n,this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce("Type change cancelled"))}handleTypeChange(e,t,n){const o=e.dataset.currentType||t.dataset.initialValue,a=this.collectFormData(e),s=window.getTemplate(`seo-${n}`);if(!s)return void console.error("No template found for type:",n);const i=e.querySelector(".seo-"+o);if(i&&(i.parentNode.insertBefore(s,i),i.remove()),e.dataset.currentType=n,window.jvbPopulate){this.populate=window.jvbPopulate.populate,Object.keys(a).forEach((t=>{const n=e.querySelector(`[data-field="${t}"]`);if(n){const e=a[t];this.populate(n,t,e)}}));const t=`Schema type changed to ${n}.`;this.a11y&&"function"==typeof this.a11y.announce&&this.a11y.announce(t)}}collectFormData(e){const t={},n=new FormData(e);for(let[e,o]of n.entries())if("type"!==e&&"context"!==e)if(e.includes(":")){const n=e.split(":"),a=n[0],s=parseInt(n[1]),i=n[2];t[a]||(t[a]=[]),t[a][s]||(t[a][s]={}),t[a][s][i]=o}else t[e]=o;return t}getFieldType(e){return e.classList.contains("repeater")?"repeater":"text"}addPreservedFieldStyles(){const e=document.createElement("style");e.textContent="\n            .value-preserved {\n                background-color: #e7f5e7 !important;\n                transition: background-color 0.3s ease;\n            }\n        ",document.head.appendChild(e)}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbSchema=new e)}))}))})();
\ No newline at end of file
diff --git a/inc/managers/SEO/SEOAdminPage.php b/inc/managers/SEO/SEOAdminPage.php
index 633fbaf..63b8671 100644
--- a/inc/managers/SEO/SEOAdminPage.php
+++ b/inc/managers/SEO/SEOAdminPage.php
@@ -146,7 +146,9 @@
 						echo '<div class="seo-'.$type.'">';
 					}
 					$fieldConfig = $this->registry->getFieldDefinition($fieldName);
-
+					if (!$fieldConfig) {
+						continue;
+					}
 					echo Form::render($fieldName, $config[$fieldName]??'', $fieldConfig);
 					if ($index === 0 && $fieldName === 'type') {
 						echo '<div class="seo-'.$type.'">';
@@ -267,6 +269,9 @@
 					$fields = $this->registry->getFieldsForType($type);
 					foreach ($fields as $fieldName) {
 						$config = $this->registry->getFieldDefinition($fieldName);
+						if (!$config) {
+							continue;
+						}
 						echo Form::render($fieldName, '', $config);
 					}
 					?>
diff --git a/inc/managers/SEO/SchemaBuilder.php b/inc/managers/SEO/SchemaBuilder.php
index 7f8785c..51eeba7 100644
--- a/inc/managers/SEO/SchemaBuilder.php
+++ b/inc/managers/SEO/SchemaBuilder.php
@@ -47,10 +47,10 @@
 	private array $metaFields = ['metaTitle', 'metaDescription', 'socialPreviewImage', 'twitterImage'];
 
 	private array $defaultMetaValues = [
-		'title' => '{{post_title}} | {{site_name}}',
-		'description' => '{{post_excerpt}}',
-		'image' => '{{featured_image}}',
-		'twitter_image' => ''
+		'metaTitle'          => '{{post_title}} | {{site_name}}',
+		'metaDescription'    => '{{post_excerpt}}',
+		'socialPreviewImage' => '{{featured_image}}',
+		'twitterImage'       => ''
 	];
 
 	public static function getInstance(): self
@@ -124,17 +124,10 @@
 	 */
 	public function getFieldDefinition(string $fieldName): ?array
 	{
-		$definitions = $this->getFieldDefinitions();
+		$definitions = apply_filters(BASE . 'schema_field_definitions', $this->fieldDefinitions);
 		return $definitions[$fieldName] ?? null;
 	}
 
-	/**
-	 * Get all field definitions
-	 */
-	public function getFieldDefinitions(): array
-	{
-		return apply_filters(BASE . 'schema_field_definitions', $this->fieldDefinitions);
-	}
 
 	/**
 	 * Get type definition
diff --git a/inc/managers/SEO/SchemaOutputManager.php b/inc/managers/SEO/SchemaOutputManager.php
index 649b6b2..f06ab10 100644
--- a/inc/managers/SEO/SchemaOutputManager.php
+++ b/inc/managers/SEO/SchemaOutputManager.php
@@ -132,12 +132,12 @@
 
 		$metaConfig = $this->config->meta();
 
-		if (empty($metaConfig['title'])) {
+		if (empty($metaConfig['metaTitle'])) {
 			return $title;
 		}
 
 		$resolver = $this->getResolver();
-		$customTitle = $resolver->resolve($metaConfig['title']);
+		$customTitle = $resolver->resolve($metaConfig['metaTitle']);
 
 		return $customTitle ?: $title;
 	}
@@ -158,12 +158,12 @@
 
 		$metaConfig = $this->config->meta();
 
-		if (empty($metaConfig['description'])) {
+		if (empty($metaConfig['metaDescription'])) {
 			return $description;
 		}
 
 		$resolver = $this->getResolver();
-		$customDescription = $resolver->resolve($metaConfig['description']);
+		$customDescription = $resolver->resolve($metaConfig['metaDescription']);
 
 		// Truncate to reasonable length
 		if (strlen($customDescription) > 160) {
@@ -190,16 +190,16 @@
 		$metaConfig = $this->config->meta();
 
 		// Check for custom image
-		if (!empty($metaConfig['image'])) {
+		if (!empty($metaConfig['socialPreviewImage'])) {
 			$resolver = $this->getResolver();
-			$imageUrl = $resolver->resolve($metaConfig['image']);
+			$imageUrl = $resolver->resolve($metaConfig['socialPreviewImage']);
 
 			if ($imageUrl) {
 				$params['og:image'] = $imageUrl;
 
 				// Use twitter-specific image if set, otherwise use main image
-				if (!empty($metaConfig['twitter_image'])) {
-					$twitterImage = $resolver->resolve($metaConfig['twitter_image']);
+				if (!empty($metaConfig['twitterImage'])) {
+					$twitterImage = $resolver->resolve($metaConfig['twitterImage']);
 					$params['twitter:image'] = $twitterImage ?: $imageUrl;
 				} else {
 					$params['twitter:image'] = $imageUrl;
@@ -533,12 +533,12 @@
 			$resolver = $this->getResolver();
 			$metaConfig = $this->config->meta();
 
-			if (!empty($metaConfig['title'])) {
-				$webpage['name'] = $resolver->resolve($metaConfig['title']);
+			if (!empty($metaConfig['metaTitle'])) {
+				$webpage['name'] = $resolver->resolve($metaConfig['metaTitle']);
 			}
 
-			if (!empty($metaConfig['description'])) {
-				$webpage['description'] = $resolver->resolve($metaConfig['description']);
+			if (!empty($metaConfig['metaDescription'])) {
+				$webpage['description'] = $resolver->resolve($metaConfig['metaDescription']);
 			}
 		}
 

--
Gitblit v1.10.0