Jake Vanderwerf
2026-02-09 83985b1f1534d70cca59edb01627638c890116cb
=Schema Updates
5 files modified
59 ■■■■■ changed files
assets/js/concise/SchemaManager.js 9 ●●●● patch | view | raw | blame | history
assets/js/min/schema.min.js 2 ●●● patch | view | raw | blame | history
inc/managers/SEO/SEOAdminPage.php 7 ●●●● patch | view | raw | blame | history
inc/managers/SEO/SchemaBuilder.php 17 ●●●● patch | view | raw | blame | history
inc/managers/SEO/SchemaOutputManager.php 24 ●●●● patch | view | raw | blame | history
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);
            }
        }
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)}))}))})();
(()=>{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)}))}))})();
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);
                    }
                    ?>
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
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']);
            }
        }