From c19264ac916707096fe294d996a1b7fb85206b34 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 08 Mar 2026 19:55:31 +0000
Subject: [PATCH] =Furthering along the complete refactor. Added some basic setup for is_content taxonomies, including a page for an archive of the terms.

---
 inc/managers/SEO/render/Traits/_Properties/artformTrait.php                |   21 
 inc/registrar/config/seo/Helpers.php                                       |    5 
 inc/managers/SEO/render/SchemaOutput.php                                   |   31 
 assets/css/dash.min.css                                                    |    2 
 jvb.php                                                                    |    6 
 inc/registrar/Terms.php                                                    |  141 +-
 inc/meta/Form.php                                                          |    6 
 inc/rest/routes/ContentRoutes.php                                          |    5 
 inc/managers/CRUDManager.php                                               |    2 
 inc/registrar/config/seo/Resolver.php                                      |   75 +
 inc/blocks/CustomBlocks.php                                                |    8 
 inc/registrar/_setup.php                                                   |    6 
 assets/js/concise/SimpleCache.js                                           |    2 
 inc/meta/MetaFormOld.php                                                   |    2 
 inc/registrar/config/seo/Schema.php                                        |  207 +++
 assets/js/min/cache.min.js                                                 |    2 
 inc/managers/SEO/render/Traits/_Properties/aboutTrait.php                  |   29 
 inc/managers/SEO/render/Traits/_Properties/_setup.php                      |    9 
 inc/ui/CRUDSkeleton.php                                                    |   22 
 inc/integrations/Integrations.php                                          |    3 
 inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php              |   21 
 inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php        |    2 
 inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php |   28 
 inc/registrar/fields/UploadField.php                                       |    2 
 inc/managers/SEO/render/Traits/_Properties/inkerTrait.php                  |   23 
 inc/registrar/fields/RepeaterField.php                                     |   50 +
 inc/admin/SEOAdmin.php                                                     |    1 
 inc/managers/SEO/render/Traits/_Properties/artistTrait.php                 |   23 
 inc/managers/SEO/render/Traits/_Properties/creatorTrait.php                |   11 
 inc/managers/DirectoryManager.php                                          |    3 
 inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php     |    9 
 cleanup.php                                                                |   45 +
 inc/registrar/Registrar.php                                                |  117 ++
 inc/meta/Storage.php                                                       |    2 
 inc/registrar/fields/SelectorField.php                                     |  125 ++
 inc/registrar/fields/TagListField.php                                      |   33 
 inc/managers/SEO/render/Thing/CreativeWork/_setup.php                      |    1 
 inc/meta/MetaTypeManager.php                                               |   14 
 inc/managers/ReferralManager.php                                           |    4 
 inc/managers/SEO/_edmonotonink.php                                         |  980 +++++++++++-----------
 inc/registrar/fields/GroupedField.php                                      |   12 
 inc/meta/Sanitizer.php                                                     |   30 
 assets/js/concise/AuthManager.js                                           |    4 
 assets/js/min/auth.min.js                                                  |    2 
 inc/registrar/Fields.php                                                   |  198 ++++
 inc/meta/Item.php                                                          |    2 
 inc/registrar/fields/Field.php                                             |   17 
 inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php             |   21 
 inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php               |   23 
 inc/managers/ScriptLoader.php                                              |    2 
 inc/managers/SEO/render/Traits/_Properties/coloristTrait.php               |   23 
 assets/js/concise/DataStore.js                                             |    2 
 inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php          |    1 
 inc/managers/SEO/render/Traits/ThingSchema.php                             |   12 
 inc/registrar/config/SEO.php                                               |   19 
 /dev/null                                                                  |   19 
 assets/js/min/dataStore.min.js                                             |    2 
 inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php            |   13 
 inc/managers/SEO/render/Traits/_Properties/lettererTrait.php               |   23 
 inc/users/UserSettings.php                                                 |   10 
 inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php         |   21 
 inc/meta/Repeater.php                                                      |    2 
 62 files changed, 1,831 insertions(+), 705 deletions(-)

diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index 583ee8c..d557a97 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-:target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:0;bottom:0}.dashboard>header{justify-content:flex-end;position:fixed}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}.list-view h3{font-size:var(--txt-medium)}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%;display:flex;justify-content:space-between}.all-filters summary [data-action=clear-filters]{--w:1em!important;width:max-content;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]{margin-left:auto;--w:1em!important;flex-wrap:nowrap;justify-content:flex-start;transition:var(--trans-size);display:flex;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]:focus,.all-filters [data-action=refresh]:hover{width:max-content}.all-filters [data-action=refresh] span{display:none;white-space:nowrap}.all-filters [data-action=refresh]:focus span,.all-filters [data-action=refresh]:hover span{display:block}.all-filters .btn+label{box-shadow:var(--shdw-none);color:var(--base-200)}.all-filters .radio-options input:not(.ch):checked+label{box-shadow:rgba(var(--base-rgb),var(--op-6)) var(--shdw-inset);color:var(--contrast-200);border-color:var(--contrast-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chip_);padding:.125rem!important;min-width:0;min-height:var(--chip_);width:var(--chip_)}.all-filters>.row{padding:.25rem 0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.crud form.table td .label,.crud form.table td label:not(.select-item-label):not(.radio-option){display:none}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}.dashboard #queue{bottom:0}
\ No newline at end of file
+:target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:0;bottom:0}.dashboard>header{justify-content:flex-end;position:fixed}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}.list-view h3{font-size:var(--txt-medium)}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%;display:flex;justify-content:space-between}.all-filters summary [data-action=clear-filters]{--w:1em!important;width:max-content;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]{margin-left:auto;--w:1em!important;flex-wrap:nowrap;justify-content:flex-start;transition:var(--trans-size);display:flex;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]:focus,.all-filters [data-action=refresh]:hover{width:max-content}.all-filters [data-action=refresh] span{display:none;white-space:nowrap}.all-filters [data-action=refresh]:focus span,.all-filters [data-action=refresh]:hover span{display:block}.all-filters .btn+label{box-shadow:var(--shdw-none);color:var(--base-200)}.all-filters .radio-options input:not(.ch):checked+label{box-shadow:rgba(var(--base-rgb),var(--op-6)) var(--shdw-inset);color:var(--contrast-200);border-color:var(--contrast-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chip_);padding:.125rem!important;min-width:0;min-height:var(--chip_);width:var(--chip_)}.all-filters>.row{padding:.25rem 0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.crud form.table td .label,.crud form.table td label:not(.select-item-label):not(.radio-option){display:none}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:98vh;width:98vw;max-width:none;max-height:none;inset:0;margin:auto}dialog>.wrap{min-height:100%}dialog .item.upload.upload{display:flex;gap:1rem}dialog .item.upload .preview{width:40%}dialog .item.upload .group{width:60%}.upload details{width:100%}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--txt-small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--radius-outer);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}.dashboard #queue{bottom:0}
\ No newline at end of file
diff --git a/assets/js/concise/AuthManager.js b/assets/js/concise/AuthManager.js
index 25db258..3d8c381 100644
--- a/assets/js/concise/AuthManager.js
+++ b/assets/js/concise/AuthManager.js
@@ -17,8 +17,8 @@
 		this.nonces = {};
 
 		this.subscribers = new Set();
-		this.storageKey = 'jvb_auth_state';
-		this.cacheMetaKey = 'jvb_auth_meta';
+		this.storageKey = `${jvbBase.base}auth_state`;
+		this.cacheMetaKey = `${jvbBase.base}auth_meta`;
 		this.cacheExpiry = 5 * 60 * 1000; // 5 minutes
 
 		this.init();
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 29dc9c7..7948f97 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -51,7 +51,7 @@
 
 		if (!this.dbConfig.has(name)) {
 			this.dbConfig.set(name, {
-				dbName: `jvb_${name}`,
+				dbName: `${jvbBase.base}${name}`,
 				version: version,
 				stores: {},
 				_initialized: false
diff --git a/assets/js/concise/SimpleCache.js b/assets/js/concise/SimpleCache.js
index 30f074c..e33fd14 100644
--- a/assets/js/concise/SimpleCache.js
+++ b/assets/js/concise/SimpleCache.js
@@ -18,7 +18,7 @@
 	constructor(base, config = {}) {
 		this.base = base;
 		this.config = {
-			namespace: 'jvb_cache',
+			namespace: `${jvbBase.base}cache`,
 			TTL: 3600000,
 			maxSize: 100,
 			...config
diff --git a/assets/js/min/auth.min.js b/assets/js/min/auth.min.js
index 7a8b15d..bc6fa26 100644
--- a/assets/js/min/auth.min.js
+++ b/assets/js/min/auth.min.js
@@ -1 +1 @@
-window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.storageKey="jvb_auth_state",this.cacheMetaKey="jvb_auth_meta",this.cacheExpiry=3e5,this.init()}async init(){if(this.isAuthenticating)return this.ready();this.isAuthenticating=!0;try{const t=this.getCachedAuth();if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!0});await this.fetchAuth()}catch(t){console.error("Failed to initialize auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,e={}){const i=async(s=0)=>{const a={"Content-Type":"application/json",...e.headers,"X-WP-Nonce":this.getNonce()},h=await fetch(t,{...e,credentials:"same-origin",headers:a});if((403===h.status||401===h.status)&&0===s){const t=await h.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),i(1)}return h};return i()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const e=await t.json(),i=sessionStorage.getItem(this.cacheMetaKey);if(i){const t=JSON.parse(i);t.session_id&&t.session_id!==e.session_id&&(this.clearCachedAuth(),this.notify("session-changed",{}))}this.cacheAuth(e),this.setAuthData(e),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{}}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={},sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}getCachedAuth(){try{const t=sessionStorage.getItem(this.storageKey),e=sessionStorage.getItem(this.cacheMetaKey);if(!t||!e)return null;const i=JSON.parse(e),s=JSON.parse(t);return Date.now()-i.timestamp>this.cacheExpiry?(this.clearCachedAuth(),null):s}catch(t){return console.error("Error reading cached auth:",t),null}}cacheAuth(t){try{sessionStorage.setItem(this.storageKey,JSON.stringify(t)),sessionStorage.setItem(this.cacheMetaKey,JSON.stringify({session_id:t.session_id||null,timestamp:Date.now()}))}catch(t){console.error("Error caching auth:",t)}}clearCachedAuth(){sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey),t)return this.cacheAuth(t),this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((i=>{try{i(t,e)}catch(t){console.error("Subscriber error:",t)}}))}ready(){return this.initialized?Promise.resolve():new Promise((t=>{const e=this.subscribe((i=>{"auth-loaded"!==i&&"auth-error"!==i||(e(),t())}))}))}};
\ No newline at end of file
+window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.storageKey=`${jvbBase.base}auth_state`,this.cacheMetaKey=`${jvbBase.base}auth_meta`,this.cacheExpiry=3e5,this.init()}async init(){if(this.isAuthenticating)return this.ready();this.isAuthenticating=!0;try{const t=this.getCachedAuth();if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!0});await this.fetchAuth()}catch(t){console.error("Failed to initialize auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,e={}){const i=async(s=0)=>{const a={"Content-Type":"application/json",...e.headers,"X-WP-Nonce":this.getNonce()},h=await fetch(t,{...e,credentials:"same-origin",headers:a});if((403===h.status||401===h.status)&&0===s){const t=await h.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),i(1)}return h};return i()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const e=await t.json(),i=sessionStorage.getItem(this.cacheMetaKey);if(i){const t=JSON.parse(i);t.session_id&&t.session_id!==e.session_id&&(this.clearCachedAuth(),this.notify("session-changed",{}))}this.cacheAuth(e),this.setAuthData(e),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{}}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={},sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}getCachedAuth(){try{const t=sessionStorage.getItem(this.storageKey),e=sessionStorage.getItem(this.cacheMetaKey);if(!t||!e)return null;const i=JSON.parse(e),s=JSON.parse(t);return Date.now()-i.timestamp>this.cacheExpiry?(this.clearCachedAuth(),null):s}catch(t){return console.error("Error reading cached auth:",t),null}}cacheAuth(t){try{sessionStorage.setItem(this.storageKey,JSON.stringify(t)),sessionStorage.setItem(this.cacheMetaKey,JSON.stringify({session_id:t.session_id||null,timestamp:Date.now()}))}catch(t){console.error("Error caching auth:",t)}}clearCachedAuth(){sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey),t)return this.cacheAuth(t),this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((i=>{try{i(t,e)}catch(t){console.error("Subscriber error:",t)}}))}ready(){return this.initialized?Promise.resolve():new Promise((t=>{const e=this.subscribe((i=>{"auth-loaded"!==i&&"auth-error"!==i||(e(),t())}))}))}};
\ No newline at end of file
diff --git a/assets/js/min/cache.min.js b/assets/js/min/cache.min.js
index 1df21e6..b75ead6 100644
--- a/assets/js/min/cache.min.js
+++ b/assets/js/min/cache.min.js
@@ -1 +1 @@
-window.jvbCache=class{constructor(e,t={}){this.base=e,this.config={namespace:"jvb_cache",TTL:36e5,maxSize:100,...t},this._cache=new Map,this.subscribers=new Set}clearMemoryCache(){const e=this._cache.size;return this._cache.clear(),console.log(`Cleared ${e} items from memory cache`),e}get(e){if(this._cache.has(e))return this._cache.get(e);let t,a=`${this.base}_${e}`;try{if(t=localStorage.getItem(a),!t)return null;t=JSON.parse(t)}catch(e){return console.warn("Error getting from localStorage:",e),null}return t&&this._cache.set(e,t),t}set(e,t){this._cache.set(e,t);let a=`${this.base}_${e}`;try{localStorage.setItem(a,JSON.stringify(t))}catch(t){if(t instanceof DOMException&&22===t.code){this.clearOldestLocalStorageItems();try{localStorage.setItem(e,JSON.stringify(item))}catch(e){console.warn("Still failed to set localStorage item after cleanup:",e)}}else console.warn("Error setting localStorage item:",t)}this.notify("cache-saved",{key:e,value:t})}remove(e){let t=`${this.base}_${e}`;try{localStorage.removeItem(t)}catch(e){console.warn("Error removing localStorage item:",e)}}clearOldestLocalStorageItems(){try{const e=[];for(let t=0;t<localStorage.length;t++){const a=localStorage.key(t);if(a.startsWith(this.config.namespace))try{const t=JSON.parse(localStorage.getItem(a));e.push({key:a,timestamp:t.timestamp||0})}catch(t){e.push({key:a,timestamp:0})}}e.sort(((e,t)=>e.timestamp-t.timestamp));const t=Math.max(1,Math.ceil(.2*e.length));for(let a=0;a<t;a++)e[a]&&localStorage.removeItem(e[a].key)}catch(e){console.warn("Error cleaning up localStorage:",e)}}async loadFromCache(){for(let e=0;e<localStorage.length;e++){const t=localStorage.key(e);if(t.startsWith(`${this.base}_`)){let e=t.replace(`${this.base}_`,"");try{const a=JSON.parse(localStorage.getItem(t));this._cache.set(e,a)}catch(e){console.warn(`Failed to parse cached value for ${t}:`,e)}}}}async clear(){this._cache.clear();try{for(let e=localStorage.length-1;e>=0;e--){const t=localStorage.key(e);t&&t.startsWith(this.config.namespace)&&localStorage.removeItem(t)}}catch(e){console.warn("Error clearing localStorage cache:",e)}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((a=>a(e,t)))}};
\ No newline at end of file
+window.jvbCache=class{constructor(e,t={}){this.base=e,this.config={namespace:`${jvbBase.base}cache`,TTL:36e5,maxSize:100,...t},this._cache=new Map,this.subscribers=new Set}clearMemoryCache(){const e=this._cache.size;return this._cache.clear(),console.log(`Cleared ${e} items from memory cache`),e}get(e){if(this._cache.has(e))return this._cache.get(e);let t,a=`${this.base}_${e}`;try{if(t=localStorage.getItem(a),!t)return null;t=JSON.parse(t)}catch(e){return console.warn("Error getting from localStorage:",e),null}return t&&this._cache.set(e,t),t}set(e,t){this._cache.set(e,t);let a=`${this.base}_${e}`;try{localStorage.setItem(a,JSON.stringify(t))}catch(t){if(t instanceof DOMException&&22===t.code){this.clearOldestLocalStorageItems();try{localStorage.setItem(e,JSON.stringify(item))}catch(e){console.warn("Still failed to set localStorage item after cleanup:",e)}}else console.warn("Error setting localStorage item:",t)}this.notify("cache-saved",{key:e,value:t})}remove(e){let t=`${this.base}_${e}`;try{localStorage.removeItem(t)}catch(e){console.warn("Error removing localStorage item:",e)}}clearOldestLocalStorageItems(){try{const e=[];for(let t=0;t<localStorage.length;t++){const a=localStorage.key(t);if(a.startsWith(this.config.namespace))try{const t=JSON.parse(localStorage.getItem(a));e.push({key:a,timestamp:t.timestamp||0})}catch(t){e.push({key:a,timestamp:0})}}e.sort(((e,t)=>e.timestamp-t.timestamp));const t=Math.max(1,Math.ceil(.2*e.length));for(let a=0;a<t;a++)e[a]&&localStorage.removeItem(e[a].key)}catch(e){console.warn("Error cleaning up localStorage:",e)}}async loadFromCache(){for(let e=0;e<localStorage.length;e++){const t=localStorage.key(e);if(t.startsWith(`${this.base}_`)){let e=t.replace(`${this.base}_`,"");try{const a=JSON.parse(localStorage.getItem(t));this._cache.set(e,a)}catch(e){console.warn(`Failed to parse cached value for ${t}:`,e)}}}}async clear(){this._cache.clear();try{for(let e=localStorage.length-1;e>=0;e--){const t=localStorage.key(e);t&&t.startsWith(this.config.namespace)&&localStorage.removeItem(t)}}catch(e){console.warn("Error clearing localStorage cache:",e)}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((a=>a(e,t)))}};
\ No newline at end of file
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index 35242db..0ee0509 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.25){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order","context","source",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>{const e=o.error||new Error("Transaction failed with unknown error");a(e)};try{h=r(c,o)}catch(e){a(e||new Error("Callback failed with unknown error"))}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL)){let t=r.items.map((t=>this.get(e,t)));return this.notify(e,"data-loaded",{cached:!0,items:t??[]}),r}t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){if(!("AbortError"===t?.name))throw console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t}),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=(t.items||[]).filter((e=>e&&"object"==typeof e)),o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}};for(let[t,s]of Object.entries(i.filters))"string"==typeof s&&s.includes(",")&&this.createSplitCacheEntries(e,a,t,i.filters,r);o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}createSplitCacheEntries(e,t,s,r,i){const a=this.stores.get(e);r[s].split(",").map((e=>e.trim())).forEach((t=>{let o={};o[s]=t;const n={...r,[s]:t},c=this.generateCacheKey(n);if(a.cache.has(c))return;let h=this.filterByIndex(e,o).map((e=>this.getItemKey(e,a.config.keyPath)));const l={key:c,items:h,timestamp:Date.now(),endpoint:a.config.endpoint,filters:n,etag:i.headers.get("Etag"),lastModified:i.headers.get("Last-Modified"),has_more:20===h.length};a.cache.set(c,l),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(l)}))}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach((t=>{const s=this._saveItem(e,t);i.push(s)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{i.forEach((t=>{e.put(t.processed)}))})),this.notify(e,"items-saved",{count:i.length,keys:i.map((e=>e.key))}),i.map((e=>e.key))}processForStorage(e,t=!0,s="root"){if(null===e)return{valid:!0,data:null};if(void 0===e)return t?{valid:!1,error:`Undefined value at ${s}`}:{valid:!0,data:void 0};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0===e.data&&null!==a||(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach((e=>{s.data.delete(e)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{r.forEach((t=>{e.delete(t)}))})),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce(((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e}),[]):i.map((e=>r.data.get(e)??null))}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>!(!e||"object"!=typeof e)&&Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items){const e=r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);return this.applyOrdering(e,t)}const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];t.filters.taxonomy&&"object"==typeof t.filters.taxonomy&&Object.entries(t.filters.taxonomy).forEach((([e,t])=>{const s=Array.isArray(t)?t:[t];o.push((t=>{if(!t.taxonomies||!t.taxonomies[e])return!1;const r=Object.keys(t.taxonomies[e]).map((e=>parseInt(e)));return s.some((e=>r.includes(parseInt(e))))}))}));for(const[e,s]of Object.entries(t.filters))if("taxonomy"!==e&&!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map((e=>e.trim()));o.push((s=>t.includes(String(s[e]))))}else o.push((t=>String(t[e])===String(s)));const n=i.filter((e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();return["random","rand"].includes(s)||["random","rand"].includes(r)?this.shuffle(e):(e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.title||e.name||"").toLowerCase(),a=(t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||e.date||0),a=new Date(t.modified||t.date||0);break;default:i=new Date(e.date||e.modified||0),a=new Date(t.date||t.modified||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0})),e)}shuffle(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t});const a=await this.shouldFetchWithFilters(e,t,i);if(r.config.endpoint&&a)await this.fetch(e);else{const t=this.getFiltered(e);this.notify(e,"data-loaded",{cached:!0,items:t})}}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.25){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`${jvbBase.base}${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order","context","source",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>{const e=o.error||new Error("Transaction failed with unknown error");a(e)};try{h=r(c,o)}catch(e){a(e||new Error("Callback failed with unknown error"))}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL)){let t=r.items.map((t=>this.get(e,t)));return this.notify(e,"data-loaded",{cached:!0,items:t??[]}),r}t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){if(!("AbortError"===t?.name))throw console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t}),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=(t.items||[]).filter((e=>e&&"object"==typeof e)),o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}};for(let[t,s]of Object.entries(i.filters))"string"==typeof s&&s.includes(",")&&this.createSplitCacheEntries(e,a,t,i.filters,r);o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}createSplitCacheEntries(e,t,s,r,i){const a=this.stores.get(e);r[s].split(",").map((e=>e.trim())).forEach((t=>{let o={};o[s]=t;const n={...r,[s]:t},c=this.generateCacheKey(n);if(a.cache.has(c))return;let h=this.filterByIndex(e,o).map((e=>this.getItemKey(e,a.config.keyPath)));const l={key:c,items:h,timestamp:Date.now(),endpoint:a.config.endpoint,filters:n,etag:i.headers.get("Etag"),lastModified:i.headers.get("Last-Modified"),has_more:20===h.length};a.cache.set(c,l),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(l)}))}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach((t=>{const s=this._saveItem(e,t);i.push(s)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{i.forEach((t=>{e.put(t.processed)}))})),this.notify(e,"items-saved",{count:i.length,keys:i.map((e=>e.key))}),i.map((e=>e.key))}processForStorage(e,t=!0,s="root"){if(null===e)return{valid:!0,data:null};if(void 0===e)return t?{valid:!1,error:`Undefined value at ${s}`}:{valid:!0,data:void 0};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0===e.data&&null!==a||(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach((e=>{s.data.delete(e)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{r.forEach((t=>{e.delete(t)}))})),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce(((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e}),[]):i.map((e=>r.data.get(e)??null))}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>!(!e||"object"!=typeof e)&&Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items){const e=r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);return this.applyOrdering(e,t)}const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];t.filters.taxonomy&&"object"==typeof t.filters.taxonomy&&Object.entries(t.filters.taxonomy).forEach((([e,t])=>{const s=Array.isArray(t)?t:[t];o.push((t=>{if(!t.taxonomies||!t.taxonomies[e])return!1;const r=Object.keys(t.taxonomies[e]).map((e=>parseInt(e)));return s.some((e=>r.includes(parseInt(e))))}))}));for(const[e,s]of Object.entries(t.filters))if("taxonomy"!==e&&!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map((e=>e.trim()));o.push((s=>t.includes(String(s[e]))))}else o.push((t=>String(t[e])===String(s)));const n=i.filter((e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();return["random","rand"].includes(s)||["random","rand"].includes(r)?this.shuffle(e):(e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.title||e.name||"").toLowerCase(),a=(t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||e.date||0),a=new Date(t.modified||t.date||0);break;default:i=new Date(e.date||e.modified||0),a=new Date(t.date||t.modified||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0})),e)}shuffle(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t});const a=await this.shouldFetchWithFilters(e,t,i);if(r.config.endpoint&&a)await this.fetch(e);else{const t=this.getFiltered(e);this.notify(e,"data-loaded",{cached:!0,items:t})}}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})();
\ No newline at end of file
diff --git a/cleanup.php b/cleanup.php
index e646909..8aef5fa 100644
--- a/cleanup.php
+++ b/cleanup.php
@@ -264,3 +264,48 @@
 	return $clean_classes;
 }
 add_filter('body_class', 'jvbBodyClasses');
+
+
+add_action('admin_init', function () {
+	// Redirect any user trying to access comments page
+	global $pagenow;
+
+	if ($pagenow === 'edit-comments.php') {
+		wp_redirect(admin_url());
+		exit;
+	}
+
+	// Remove comments metabox from dashboard
+	remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal');
+
+	// Disable support for comments and trackbacks in post types
+	foreach (get_post_types() as $post_type) {
+		if (post_type_supports($post_type, 'comments')) {
+			remove_post_type_support($post_type, 'comments');
+			remove_post_type_support($post_type, 'trackbacks');
+		}
+	}
+});
+
+// Close comments on the front-end
+add_filter('comments_open', '__return_false', 20, 2);
+add_filter('pings_open', '__return_false', 20, 2);
+
+// Hide existing comments
+add_filter('comments_array', '__return_empty_array', 10, 2);
+
+// Remove comments page in menu
+add_action('admin_menu', function () {
+	remove_menu_page('edit-comments.php');
+});
+
+// Remove comments links from admin bar
+add_action('init', function () {
+	if (is_admin_bar_showing()) {
+		remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60);
+	}
+});
+// Remove comments links from admin bar
+add_action('add_admin_bar_menus', function () {
+	remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60);
+});
diff --git a/inc/admin/SEOAdmin.php b/inc/admin/SEOAdmin.php
index 055f812..6c01e01 100644
--- a/inc/admin/SEOAdmin.php
+++ b/inc/admin/SEOAdmin.php
@@ -25,6 +25,7 @@
 		'JVBase\managers\SEO\render\Thing\CreativeWork\MusicRecording'		=> ' - - Music Recording',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\Photograph'			=> ' - - Photograph',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\Review'				=> ' - - Review',
+		'JVBase\managers\SEO\render\Thing\CreativeWork\VisualArtwork\VisualArtwork'		=> ' - - Visual Artwork',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\Webpage'		=> ' - - WebPage',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\AboutPage'	=> ' - - - About Page',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CheckoutPage'	=> ' - - - Checkout Page',
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index 93e16ac..ffb46b8 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -18,7 +18,7 @@
     {
         $this->cache = Cache::for('blocks', WEEK_IN_SECONDS);
 		$this->cache->connect('post')->connect('taxonomy');
-		add_filter('render_block', [$this, 'render'], 999, 3);
+		add_filter('render_block', [$this, 'render'], 990, 3);
 
         add_action('init', [$this, 'registerBlockStyles']);
     }
@@ -616,10 +616,12 @@
 			global $post;
 
 			$block['innerBlocks'] = parse_blocks($post->post_content);
-			return $this->innerBlocks($block);
+			$result = $this->innerBlocks($block);
         } else {
-            return $this->inside($block, $tag, $content);
+            $result = $this->inside($block, $tag, $content);
         }
+
+		return apply_filters('jvb_post_content_output', $result, $block);
     }
     //core_post_date
 	public function render_core_post_date(array $block):string
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 54f4cf4..3a6643a 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -319,7 +319,8 @@
 		if (!$taxonomies) {
 			// Combine both content and taxonomy filtering
 			$taxonomies = [];
-			foreach (Registrar::getFeatured('is_content', 'term') as $registrar) {
+			foreach (Registrar::getFeatured('is_content', 'term') as $type) {
+				$registrar = Registrar::getInstance($type);
 				if ($registrar->hasIntegration($this->service_name)) {
 					$taxonomies[] = $registrar->getSlug();
 				}
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index 3cbf9a7..7ad84df 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -87,7 +87,7 @@
 		$this->skeleton->addDateFilter();
 		$this->skeleton->addCustomDateRange($this->addDateRanges());
 		if (!empty($this->taxonomies)) {
-			$this->skeleton->addTaxonomyFilter(array_keys($this->taxonomies), 'user');
+			$this->skeleton->addTaxonomyFilter($this->taxonomies, 'user');
 		}
 
 		// Capabilities
diff --git a/inc/managers/DirectoryManager.php b/inc/managers/DirectoryManager.php
index 46f4ef8..f336d60 100644
--- a/inc/managers/DirectoryManager.php
+++ b/inc/managers/DirectoryManager.php
@@ -37,7 +37,7 @@
 
 		jvb_register_do_once('buildDirectories', [$this, 'activate']);
 		add_action('init', [$this, 'registerDirectories']);
-        add_action('render_block', [$this, 'renderBlock'], 99999, 3);
+        add_action('render_block', [$this, 'renderBlock'], 998, 3);
     }
 
     public function registerDirectories():void
@@ -684,6 +684,7 @@
             return $content;
         }
 
+		error_log('Still working on directory manager...');
         // For archive page
         if (is_post_type_archive(BASE.'directory') && $block['blockName'] === 'core/group') {
             return ($block['attrs']['tagName']??'' === 'main') ? '<main>'.$this->renderArchive().'</main>' : $content;
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 6182a7f..48245fa 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -2491,11 +2491,11 @@
 			<p><small>(No data is stored. Your friends will get an email from our email.)</small></p>
 			<?php
 			$invite = [
-				'type' => 'tag_list',
+				'type' => 'taglist',
 				'label' => 'Invite Your Friends',
 				'hint' => 'Add friends to send them a referral link',
 				'add_label' => 'Add Invite',
-				'tag_format' => '{name} ({email})', // or 'first_field', 'all_fields', 'email', etc.
+				'tag_format' => '{{name}} ({{email}})', // or 'first_field', 'all_fields', 'email', etc.
 				'fields' => [
 					'name' => [
 						'type' => 'text',
diff --git a/inc/managers/SEO/_edmonotonink.php b/inc/managers/SEO/_edmonotonink.php
index 248f76f..6689413 100644
--- a/inc/managers/SEO/_edmonotonink.php
+++ b/inc/managers/SEO/_edmonotonink.php
@@ -42,498 +42,498 @@
 // CONTENT TYPES CONFIGURATION
 // ==================================================
 
-add_filter('the_content', function ($content) {
-
-	// ARTIST
-	$content['artist'] = array_merge($content['artist'] ?? [], [
-		'seo' => [
-			'title_template' => '{{name}} | {{primary_style}} Tattoo Artist in {{city}}',
-			'description_template' => '{{name}} is a {{primary_style}} tattoo artist {{location_text}}. {{bio}} Browse portfolio and book appointments.',
-			'variables' => [
-				'name' => 'post_title',
-				'primary_style' => ['taxonomy' => BASE . 'style', 'primary' => true],
-				'styles' => ['taxonomy' => BASE . 'style'],
-				'city' => ['taxonomy' => BASE . 'city', 'primary' => true],
-				'primary_shop' => ['taxonomy' => BASE . 'shop', 'primary' => true],
-				'bio' => ['meta' => 'bio', 'truncate' => 100],
-				'location_text' => ['callback' => function ($post_id, $context) {
-					$city_terms = get_the_terms($post_id, BASE . 'city');
-					$shop_terms = get_the_terms($post_id, BASE . 'shop');
-
-					if ($shop_terms && !is_wp_error($shop_terms)) {
-						$shop = $shop_terms[0];
-						return "working at {$shop->name}";
-					} elseif ($city_terms && !is_wp_error($city_terms)) {
-						$city = $city_terms[0];
-						return "in {$city->name}";
-					}
-					return "in Edmonton";
-				}],
-			],
-			'archive_title' => 'Edmonton\'s Best Tattoo Artists | Browse by Style & Shop',
-			'archive_description' => 'Explore Edmonton\'s top tattoo artists. Filter by style, shop, or location. View portfolios and book your next tattoo today.'
-		],
-
-		'schema' => [
-			'type' => 'Person',
-			'additional_types' => ['Artist'],
-			'properties' => [
-				'jobTitle' => ['callback' => function ($id, $context, $meta) {
-					$job = $meta['job_title'] ?? null;
-					if ($job) return $job;
-
-					// Try to build from specialties
-					$styles = get_the_terms($id, BASE . 'style');
-					if ($styles && !is_wp_error($styles) && count($styles) > 0) {
-						$primary = $styles[0]->name;
-						return "{$primary} Tattoo Artist";
-					}
-					return "Tattoo Artist";
-				}],
-				'telephone' => 'phone',
-				'email' => 'email',
-				'url' => ['callback' => function ($id) {
-					return get_permalink($id);
-				}],
-				'image' => ['callback' => function ($id) {
-					return get_the_post_thumbnail_url($id, 'full');
-				}],
-				'knowsAbout' => ['taxonomy' => BASE . 'style'],
-				'worksFor' => ['callback' => function ($id, $context, $meta) {
-					$shops = get_the_terms($id, BASE . 'shop');
-					if (!$shops || is_wp_error($shops)) {
-						return null;
-					}
-
-					return array_map(function ($shop) {
-						return [
-							'@type' => 'LocalBusiness',
-							'@id' => get_term_link($shop) . '#business',
-							'name' => $shop->name,
-							'url' => get_term_link($shop)
-						];
-					}, $shops);
-				}],
-				'memberOf' => [
-					'@id' => 'https://edmonton.ink/#organization'
-				],
-			]
-		]
-	]);
-
-	// PARTNER (Shops/Organizations)
-	$content['partner'] = array_merge($content['partner'] ?? [], [
-		'seo' => [
-			'title_template' => '{{name}} | Community Partner',
-			'description_template' => '{{name}} - {{excerpt}}',
-			'variables' => [
-				'name' => 'post_title',
-				'excerpt' => ['callback' => function ($id) {
-					return get_the_excerpt($id);
-				}],
-			],
-			'archive_title' => 'Our Community Partners | Supporting Edmonton\'s Tattoo Scene',
-			'archive_description' => 'Meet the businesses and organizations supporting Edmonton\'s tattoo community. Our partners help make edmonton.ink possible.'
-		],
-
-		'schema' => [
-			'type' => 'Organization',
-			'properties' => [
-				'name' => ['callback' => function ($id) {
-					return get_the_title($id);
-				}],
-				'description' => ['callback' => function ($id) {
-					return get_the_excerpt($id);
-				}],
-				'url' => ['callback' => function ($id, $context, $meta) {
-					$website = $meta['website'] ?? null;
-					return $website ?: get_permalink($id);
-				}],
-				'logo' => ['callback' => function ($id, $context, $meta) {
-					$image_id = $meta['image'] ?? null;
-					if ($image_id) {
-						return wp_get_attachment_image_url($image_id, 'full');
-					}
-					return get_the_post_thumbnail_url($id, 'full');
-				}],
-				'telephone' => 'phone',
-				'email' => 'email',
-				'address' => 'address',
-				'sameAs' => ['callback' => function ($id, $context, $meta) {
-					$links = [];
-					if (!empty($meta['instagram'])) $links[] = $meta['instagram'];
-					if (!empty($meta['facebook'])) $links[] = $meta['facebook'];
-					if (!empty($meta['twitter'])) $links[] = $meta['twitter'];
-					return !empty($links) ? $links : null;
-				}],
-				'memberOf' => [
-					'@id' => 'https://edmonton.ink/#organization'
-				],
-			]
-		]
-	]);
-
-	// TATTOO
-	$content['tattoo'] = array_merge($content['tattoo'] ?? [], [
-		'seo' => [
-			'title_template' => '{{style}} Tattoo by {{artist}} | Edmonton',
-			'description_template' => 'Beautiful {{style}} tattoo by {{artist}}{{location}}. View more work from Edmonton\'s talented artists.',
-			'variables' => [
-				'style' => ['taxonomy' => BASE . 'style', 'primary' => true],
-				'artist' => ['callback' => function ($id) {
-					$artist_id = get_post_meta($id, BASE . 'link', true);
-					if ($artist_id) {
-						return get_the_title($artist_id);
-					}
-					return 'Edmonton Artist';
-				}],
-				'location' => ['callback' => function ($id) {
-					$artist_id = get_post_meta($id, BASE . 'link', true);
-					if (!$artist_id) return '';
-
-					$shops = get_the_terms($artist_id, BASE . 'shop');
-					if ($shops && !is_wp_error($shops)) {
-						return ' at ' . $shops[0]->name;
-					}
-					return '';
-				}],
-			],
-			'archive_title' => 'Edmonton Tattoos | Browse by Style, Artist & Shop',
-			'archive_description' => 'Browse tattoos from Edmonton\'s best artists. Filter by style, theme, or artist to find inspiration for your next piece.'
-		],
-
-		'schema' => [
-			'type' => 'CreativeWork',
-			'additional_types' => ['VisualArtwork'],
-			'properties' => [
-				'creator' => ['callback' => function ($id) {
-					$artist_id = get_post_meta($id, BASE . 'link', true);
-					if ($artist_id) {
-						return [
-							'@type' => 'Person',
-							'@id' => get_permalink($artist_id) . '#person',
-							'name' => get_the_title($artist_id),
-							'url' => get_permalink($artist_id)
-						];
-					}
-					return null;
-				}],
-				'image' => ['callback' => function ($id) {
-					return get_the_post_thumbnail_url($id, 'full');
-				}],
-				'about' => ['taxonomy' => BASE . 'theme'],
-				'artform' => 'Tattoo',
-			]
-		]
-	]);
-
-	// PIERCING
-	$content['piercing'] = array_merge($content['piercing'] ?? [], [
-		'seo' => [
-			'title_template' => '{{type}} Piercing by {{artist}} | Edmonton',
-			'description_template' => 'Professional {{type}} piercing by {{artist}}. View more piercing work from Edmonton\'s skilled professionals.',
-			'variables' => [
-				'type' => ['taxonomy' => BASE . 'pstyle', 'primary' => true],
-				'artist' => ['callback' => function ($id) {
-					$artist_id = get_post_meta($id, BASE . 'link', true);
-					return $artist_id ? get_the_title($artist_id) : 'Edmonton Professional';
-				}],
-			]
-		],
-
-		'schema' => [
-			'type' => 'CreativeWork',
-			'properties' => [
-				'creator' => ['callback' => function ($id) {
-					$artist_id = get_post_meta($id, BASE . 'link', true);
-					if ($artist_id) {
-						return [
-							'@type' => 'Person',
-							'@id' => get_permalink($artist_id) . '#person',
-							'name' => get_the_title($artist_id),
-							'url' => get_permalink($artist_id)
-						];
-					}
-					return null;
-				}],
-				'image' => ['callback' => function ($id) {
-					return get_the_post_thumbnail_url($id, 'full');
-				}],
-			]
-		]
-	]);
-
-	// EVENT
-	$content['event'] = array_merge($content['event'] ?? [], [
-		'seo' => [
-			'title_builder' => function ($post_id, $meta) {
-				$title = get_the_title($post_id);
-				$date = $meta->getValue('event_date');
-				if ($date) {
-					$formatted_date = date('F j, Y', strtotime($date));
-					return "{$title} | {$formatted_date}";
-				}
-				return "{$title} | Edmonton Tattoo Event";
-			},
-			'description_builder' => function ($post_id, $meta) {
-				$excerpt = get_the_excerpt($post_id);
-				$venue = $meta->getValue('venue');
-				$date = $meta->getValue('event_date');
-
-				$desc = $excerpt;
-				if ($venue) $desc .= " Location: {$venue}.";
-				if ($date) {
-					$formatted = date('F j, Y', strtotime($date));
-					$desc .= " Date: {$formatted}.";
-				}
-				return $desc;
-			}
-		],
-
-		'schema' => [
-			'custom_builder' => function ($post_id) {
-				$meta = Meta::forPost($post_id);
-
-				$schema = [
-					'@type' => 'Event',
-					'name' => get_the_title($post_id),
-					'url' => get_permalink($post_id),
-				];
-
-				$date = $meta->get('event_date');
-				if ($date) {
-					$schema['startDate'] = date('c', strtotime($date));
-				}
-
-				$venue = $meta->get('venue');
-				$venue_address = $meta->get('venue_address');
-				if ($venue) {
-					$schema['location'] = [
-						'@type' => 'Place',
-						'name' => $venue,
-					];
-					if ($venue_address) {
-						$schema['location']['address'] = $venue_address;
-					}
-				}
-
-				$image_id = get_post_thumbnail_id($post_id);
-				if ($image_id) {
-					$schema['image'] = wp_get_attachment_image_url($image_id, 'full');
-				}
-
-				return $schema;
-			}
-		]
-	]);
-
-	return $content;
-});
+//add_filter('the_content', function ($content) {
+//
+//	// ARTIST
+//	$content['artist'] = array_merge($content['artist'] ?? [], [
+//		'seo' => [
+//			'title_template' => '{{name}} | {{primary_style}} Tattoo Artist in {{city}}',
+//			'description_template' => '{{name}} is a {{primary_style}} tattoo artist {{location_text}}. {{bio}} Browse portfolio and book appointments.',
+//			'variables' => [
+//				'name' => 'post_title',
+//				'primary_style' => ['taxonomy' => BASE . 'style', 'primary' => true],
+//				'styles' => ['taxonomy' => BASE . 'style'],
+//				'city' => ['taxonomy' => BASE . 'city', 'primary' => true],
+//				'primary_shop' => ['taxonomy' => BASE . 'shop', 'primary' => true],
+//				'bio' => ['meta' => 'bio', 'truncate' => 100],
+//				'location_text' => ['callback' => function ($post_id, $context) {
+//					$city_terms = get_the_terms($post_id, BASE . 'city');
+//					$shop_terms = get_the_terms($post_id, BASE . 'shop');
+//
+//					if ($shop_terms && !is_wp_error($shop_terms)) {
+//						$shop = $shop_terms[0];
+//						return "working at {$shop->name}";
+//					} elseif ($city_terms && !is_wp_error($city_terms)) {
+//						$city = $city_terms[0];
+//						return "in {$city->name}";
+//					}
+//					return "in Edmonton";
+//				}],
+//			],
+//			'archive_title' => 'Edmonton\'s Best Tattoo Artists | Browse by Style & Shop',
+//			'archive_description' => 'Explore Edmonton\'s top tattoo artists. Filter by style, shop, or location. View portfolios and book your next tattoo today.'
+//		],
+//
+//		'schema' => [
+//			'type' => 'Person',
+//			'additional_types' => ['Artist'],
+//			'properties' => [
+//				'jobTitle' => ['callback' => function ($id, $context, $meta) {
+//					$job = $meta['job_title'] ?? null;
+//					if ($job) return $job;
+//
+//					// Try to build from specialties
+//					$styles = get_the_terms($id, BASE . 'style');
+//					if ($styles && !is_wp_error($styles) && count($styles) > 0) {
+//						$primary = $styles[0]->name;
+//						return "{$primary} Tattoo Artist";
+//					}
+//					return "Tattoo Artist";
+//				}],
+//				'telephone' => 'phone',
+//				'email' => 'email',
+//				'url' => ['callback' => function ($id) {
+//					return get_permalink($id);
+//				}],
+//				'image' => ['callback' => function ($id) {
+//					return get_the_post_thumbnail_url($id, 'full');
+//				}],
+//				'knowsAbout' => ['taxonomy' => BASE . 'style'],
+//				'worksFor' => ['callback' => function ($id, $context, $meta) {
+//					$shops = get_the_terms($id, BASE . 'shop');
+//					if (!$shops || is_wp_error($shops)) {
+//						return null;
+//					}
+//
+//					return array_map(function ($shop) {
+//						return [
+//							'@type' => 'LocalBusiness',
+//							'@id' => get_term_link($shop) . '#business',
+//							'name' => $shop->name,
+//							'url' => get_term_link($shop)
+//						];
+//					}, $shops);
+//				}],
+//				'memberOf' => [
+//					'@id' => 'https://edmonton.ink/#organization'
+//				],
+//			]
+//		]
+//	]);
+//
+//	// PARTNER (Shops/Organizations)
+//	$content['partner'] = array_merge($content['partner'] ?? [], [
+//		'seo' => [
+//			'title_template' => '{{name}} | Community Partner',
+//			'description_template' => '{{name}} - {{excerpt}}',
+//			'variables' => [
+//				'name' => 'post_title',
+//				'excerpt' => ['callback' => function ($id) {
+//					return get_the_excerpt($id);
+//				}],
+//			],
+//			'archive_title' => 'Our Community Partners | Supporting Edmonton\'s Tattoo Scene',
+//			'archive_description' => 'Meet the businesses and organizations supporting Edmonton\'s tattoo community. Our partners help make edmonton.ink possible.'
+//		],
+//
+//		'schema' => [
+//			'type' => 'Organization',
+//			'properties' => [
+//				'name' => ['callback' => function ($id) {
+//					return get_the_title($id);
+//				}],
+//				'description' => ['callback' => function ($id) {
+//					return get_the_excerpt($id);
+//				}],
+//				'url' => ['callback' => function ($id, $context, $meta) {
+//					$website = $meta['website'] ?? null;
+//					return $website ?: get_permalink($id);
+//				}],
+//				'logo' => ['callback' => function ($id, $context, $meta) {
+//					$image_id = $meta['image'] ?? null;
+//					if ($image_id) {
+//						return wp_get_attachment_image_url($image_id, 'full');
+//					}
+//					return get_the_post_thumbnail_url($id, 'full');
+//				}],
+//				'telephone' => 'phone',
+//				'email' => 'email',
+//				'address' => 'address',
+//				'sameAs' => ['callback' => function ($id, $context, $meta) {
+//					$links = [];
+//					if (!empty($meta['instagram'])) $links[] = $meta['instagram'];
+//					if (!empty($meta['facebook'])) $links[] = $meta['facebook'];
+//					if (!empty($meta['twitter'])) $links[] = $meta['twitter'];
+//					return !empty($links) ? $links : null;
+//				}],
+//				'memberOf' => [
+//					'@id' => 'https://edmonton.ink/#organization'
+//				],
+//			]
+//		]
+//	]);
+//
+//	// TATTOO
+//	$content['tattoo'] = array_merge($content['tattoo'] ?? [], [
+//		'seo' => [
+//			'title_template' => '{{style}} Tattoo by {{artist}} | Edmonton',
+//			'description_template' => 'Beautiful {{style}} tattoo by {{artist}}{{location}}. View more work from Edmonton\'s talented artists.',
+//			'variables' => [
+//				'style' => ['taxonomy' => BASE . 'style', 'primary' => true],
+//				'artist' => ['callback' => function ($id) {
+//					$artist_id = get_post_meta($id, BASE . 'link', true);
+//					if ($artist_id) {
+//						return get_the_title($artist_id);
+//					}
+//					return 'Edmonton Artist';
+//				}],
+//				'location' => ['callback' => function ($id) {
+//					$artist_id = get_post_meta($id, BASE . 'link', true);
+//					if (!$artist_id) return '';
+//
+//					$shops = get_the_terms($artist_id, BASE . 'shop');
+//					if ($shops && !is_wp_error($shops)) {
+//						return ' at ' . $shops[0]->name;
+//					}
+//					return '';
+//				}],
+//			],
+//			'archive_title' => 'Edmonton Tattoos | Browse by Style, Artist & Shop',
+//			'archive_description' => 'Browse tattoos from Edmonton\'s best artists. Filter by style, theme, or artist to find inspiration for your next piece.'
+//		],
+//
+//		'schema' => [
+//			'type' => 'CreativeWork',
+//			'additional_types' => ['VisualArtwork'],
+//			'properties' => [
+//				'creator' => ['callback' => function ($id) {
+//					$artist_id = get_post_meta($id, BASE . 'link', true);
+//					if ($artist_id) {
+//						return [
+//							'@type' => 'Person',
+//							'@id' => get_permalink($artist_id) . '#person',
+//							'name' => get_the_title($artist_id),
+//							'url' => get_permalink($artist_id)
+//						];
+//					}
+//					return null;
+//				}],
+//				'image' => ['callback' => function ($id) {
+//					return get_the_post_thumbnail_url($id, 'full');
+//				}],
+//				'about' => ['taxonomy' => BASE . 'theme'],
+//				'artform' => 'Tattoo',
+//			]
+//		]
+//	]);
+//
+//	// PIERCING
+//	$content['piercing'] = array_merge($content['piercing'] ?? [], [
+//		'seo' => [
+//			'title_template' => '{{type}} Piercing by {{artist}} | Edmonton',
+//			'description_template' => 'Professional {{type}} piercing by {{artist}}. View more piercing work from Edmonton\'s skilled professionals.',
+//			'variables' => [
+//				'type' => ['taxonomy' => BASE . 'pstyle', 'primary' => true],
+//				'artist' => ['callback' => function ($id) {
+//					$artist_id = get_post_meta($id, BASE . 'link', true);
+//					return $artist_id ? get_the_title($artist_id) : 'Edmonton Professional';
+//				}],
+//			]
+//		],
+//
+//		'schema' => [
+//			'type' => 'CreativeWork',
+//			'properties' => [
+//				'creator' => ['callback' => function ($id) {
+//					$artist_id = get_post_meta($id, BASE . 'link', true);
+//					if ($artist_id) {
+//						return [
+//							'@type' => 'Person',
+//							'@id' => get_permalink($artist_id) . '#person',
+//							'name' => get_the_title($artist_id),
+//							'url' => get_permalink($artist_id)
+//						];
+//					}
+//					return null;
+//				}],
+//				'image' => ['callback' => function ($id) {
+//					return get_the_post_thumbnail_url($id, 'full');
+//				}],
+//			]
+//		]
+//	]);
+//
+//	// EVENT
+//	$content['event'] = array_merge($content['event'] ?? [], [
+//		'seo' => [
+//			'title_builder' => function ($post_id, $meta) {
+//				$title = get_the_title($post_id);
+//				$date = $meta->getValue('event_date');
+//				if ($date) {
+//					$formatted_date = date('F j, Y', strtotime($date));
+//					return "{$title} | {$formatted_date}";
+//				}
+//				return "{$title} | Edmonton Tattoo Event";
+//			},
+//			'description_builder' => function ($post_id, $meta) {
+//				$excerpt = get_the_excerpt($post_id);
+//				$venue = $meta->getValue('venue');
+//				$date = $meta->getValue('event_date');
+//
+//				$desc = $excerpt;
+//				if ($venue) $desc .= " Location: {$venue}.";
+//				if ($date) {
+//					$formatted = date('F j, Y', strtotime($date));
+//					$desc .= " Date: {$formatted}.";
+//				}
+//				return $desc;
+//			}
+//		],
+//
+//		'schema' => [
+//			'custom_builder' => function ($post_id) {
+//				$meta = Meta::forPost($post_id);
+//
+//				$schema = [
+//					'@type' => 'Event',
+//					'name' => get_the_title($post_id),
+//					'url' => get_permalink($post_id),
+//				];
+//
+//				$date = $meta->get('event_date');
+//				if ($date) {
+//					$schema['startDate'] = date('c', strtotime($date));
+//				}
+//
+//				$venue = $meta->get('venue');
+//				$venue_address = $meta->get('venue_address');
+//				if ($venue) {
+//					$schema['location'] = [
+//						'@type' => 'Place',
+//						'name' => $venue,
+//					];
+//					if ($venue_address) {
+//						$schema['location']['address'] = $venue_address;
+//					}
+//				}
+//
+//				$image_id = get_post_thumbnail_id($post_id);
+//				if ($image_id) {
+//					$schema['image'] = wp_get_attachment_image_url($image_id, 'full');
+//				}
+//
+//				return $schema;
+//			}
+//		]
+//	]);
+//
+//	return $content;
+//});
 
 // ==================================================
 // TAXONOMY CONFIGURATION
 // ==================================================
 
-add_filter('jvb_taxonomy', function ($taxonomies) {
-
-	// SHOP
-	$taxonomies['shop'] = array_merge($taxonomies['shop'] ?? [], [
-		'seo' => [
-			'title_template' => '{{name}} | Tattoo Shop in {{city}}',
-			'description_template' => '{{name}}{{tagline_text}}{{established_text}} in {{city}}. Featuring {{artist_count}} talented artists. Book your appointment today.',
-			'variables' => [
-				'name' => 'term_name',
-				'city' => ['callback' => function ($term_id, $context) {
-					$meta = Meta::forTerm($term_id);
-					$city_id = $meta->get('city');
-					if ($city_id && term_exists((int)$city_id, BASE . 'city')) {
-						$city_term = get_term($city_id, BASE . 'city');
-						if ($city_term && !is_wp_error($city_term)) {
-							return $city_term->name;
-						}
-					}
-					return 'Edmonton';
-				}],
-				'tagline_text' => ['callback' => function ($term_id) {
-					$meta = Meta::forTerm($term_id);
-					$tagline = $meta->get('tagline');
-					return $tagline ? " - {$tagline}" : '';
-				}],
-				'established_text' => ['callback' => function ($term_id) {
-					$meta = Meta::forTerm($term_id);
-					$established = $meta->get('established');
-					return $established ? " Established in {$established}" : '';
-				}],
-				'artist_count' => ['callback' => function ($term_id) {
-					$artists = get_posts([
-						'post_type' => BASE . 'artist',
-						'tax_query' => [[
-							'taxonomy' => BASE . 'shop',
-							'terms' => $term_id
-						]],
-						'posts_per_page' => -1,
-						'fields' => 'ids'
-					]);
-					return count($artists);
-				}],
-			]
-		],
-
-		'schema' => [
-			'type' => 'LocalBusiness',
-			'additional_types' => ['TattooParlor'],
-			'properties' => [
-				'address' => 'address',
-				'telephone' => 'phone',
-				'email' => 'email',
-				'openingHours' => 'hours',
-				'priceRange' => 'price_range',
-				'image' => 'logo',
-				'url' => ['callback' => function ($term_id) {
-					$meta = Meta::forTerm($term_id);
-					$website = $meta->get('website');
-					return $website ?: get_term_link($term_id);
-				}],
-				'sameAs' => ['callback' => function ($term_id) {
-					$meta = Meta::forTerm($term_id);
-					$links = [];
-					if ($ig = $meta->get('instagram')) $links[] = $ig;
-					if ($fb = $meta->get('facebook')) $links[] = $fb;
-					return !empty($links) ? $links : null;
-				}],
-				'memberOf' => [
-					'@id' => 'https://edmonton.ink/#organization'
-				],
-			]
-		]
-	]);
-
-	// STYLE
-	$taxonomies['style'] = array_merge($taxonomies['style'] ?? [], [
-		'seo' => [
-			'title_template' => 'Edmonton {{name}} Tattoo Artists | Specialists in {{name}}',
-			'description_template' => '{{name}}{{alt_names}} is a distinctive tattoo style. {{characteristics}} Find Edmonton artists specializing in {{name}} tattoos.',
-			'variables' => [
-				'name' => 'term_name',
-				'alt_names' => ['callback' => function ($term_id) {
-					$meta = Meta::forTerm($term_id);
-					$alts = $meta->get('alternate_name');
-					if (!empty($alts) && is_array($alts)) {
-						$names = array_filter(array_column($alts, 'name'));
-						if (!empty($names)) {
-							return ' (also known as ' . implode(', ', array_slice($names, 0, 2)) . ')';
-						}
-					}
-					return '';
-				}],
-				'characteristics' => ['meta' => 'characteristics', 'truncate' => 100],
-			]
-		],
-
-		'schema' => [
-			'type' => 'CreativeWork',
-			'properties' => [
-				'name' => ['callback' => function ($term_id) {
-					return get_term($term_id)->name . ' Tattoo Style';
-				}],
-				'description' => 'characteristics',
-				'about' => ['meta' => 'description'],
-				'alternateName' => ['callback' => function ($term_id) {
-					$meta = Meta::forTerm($term_id);
-					$alts = $meta->get('alternate_name');
-					if (!empty($alts) && is_array($alts)) {
-						return array_filter(array_column($alts, 'name'));
-					}
-					return null;
-				}],
-			]
-		]
-	]);
-
-	// THEME
-	$taxonomies['theme'] = array_merge($taxonomies['theme'] ?? [], [
-		'seo' => [
-			'title_template' => 'Edmonton {{name}} Tattoos | Find {{name}} Tattoo Designs',
-			'description_template' => 'Explore {{name}} tattoos, a popular motif in Edmonton\'s tattoo scene. {{similar}}Find artists specializing in {{name}} designs.',
-			'variables' => [
-				'name' => 'term_name',
-				'similar' => ['callback' => function ($term_id) {
-					$meta = Meta::forTerm($term_id);
-					$similar = $meta->get('similar');
-					if (!empty($similar)) {
-						$similar_names = [];
-						foreach ((array)$similar as $similar_id) {
-							$term = get_term($similar_id, BASE . 'theme');
-							if ($term && !is_wp_error($term)) {
-								$similar_names[] = html_entity_decode($term->name);
-							}
-						}
-						if (!empty($similar_names)) {
-							return 'Similar themes include ' . implode(', ', array_slice($similar_names, 0, 2)) . '. ';
-						}
-					}
-					return '';
-				}],
-			]
-		],
-
-		'schema' => [
-			'type' => 'CreativeWork',
-			'properties' => [
-				'name' => ['callback' => function ($term_id) {
-					return get_term($term_id)->name . ' Tattoo Theme';
-				}],
-				'description' => ['meta' => 'description'],
-			]
-		]
-	]);
-
-	// CITY
-	$taxonomies['city'] = array_merge($taxonomies['city'] ?? [], [
-		'seo' => [
-			'title_template' => '{{name}} Tattoo Artists & Shops | edmonton.ink',
-			'description_template' => 'Discover {{name}}\'s vibrant tattoo scene featuring {{shop_count}} local shops and {{artist_count}} talented artists. Find top local talent and book your next tattoo today.',
-			'variables' => [
-				'name' => 'term_name',
-				'shop_count' => ['callback' => function ($term_id) {
-					$shops = get_terms([
-						'taxonomy' => BASE . 'shop',
-						'meta_key' => BASE . 'city',
-						'meta_value' => $term_id,
-						'fields' => 'count'
-					]);
-					return is_wp_error($shops) ? 0 : $shops;
-				}],
-				'artist_count' => ['callback' => function ($term_id) {
-					$artists = get_posts([
-						'post_type' => BASE . 'artist',
-						'tax_query' => [[
-							'taxonomy' => BASE . 'city',
-							'terms' => $term_id
-						]],
-						'posts_per_page' => -1,
-						'fields' => 'ids'
-					]);
-					return count($artists);
-				}],
-			]
-		],
-
-		'schema' => [
-			'type' => 'Place',
-			'properties' => [
-				'address' => ['callback' => function ($term_id) {
-					$term = get_term($term_id);
-					return [
-						'@type' => 'PostalAddress',
-						'addressLocality' => html_entity_decode($term->name),
-						'addressRegion' => 'Alberta',
-						'addressCountry' => 'CA'
-					];
-				}],
-			]
-		]
-	]);
-
-	return $taxonomies;
-});
+//add_filter('jvb_taxonomy', function ($taxonomies) {
+//
+//	// SHOP
+//	$taxonomies['shop'] = array_merge($taxonomies['shop'] ?? [], [
+//		'seo' => [
+//			'title_template' => '{{name}} | Tattoo Shop in {{city}}',
+//			'description_template' => '{{name}}{{tagline_text}}{{established_text}} in {{city}}. Featuring {{artist_count}} talented artists. Book your appointment today.',
+//			'variables' => [
+//				'name' => 'term_name',
+//				'city' => ['callback' => function ($term_id, $context) {
+//					$meta = Meta::forTerm($term_id);
+//					$city_id = $meta->get('city');
+//					if ($city_id && term_exists((int)$city_id, BASE . 'city')) {
+//						$city_term = get_term($city_id, BASE . 'city');
+//						if ($city_term && !is_wp_error($city_term)) {
+//							return $city_term->name;
+//						}
+//					}
+//					return 'Edmonton';
+//				}],
+//				'tagline_text' => ['callback' => function ($term_id) {
+//					$meta = Meta::forTerm($term_id);
+//					$tagline = $meta->get('tagline');
+//					return $tagline ? " - {$tagline}" : '';
+//				}],
+//				'established_text' => ['callback' => function ($term_id) {
+//					$meta = Meta::forTerm($term_id);
+//					$established = $meta->get('established');
+//					return $established ? " Established in {$established}" : '';
+//				}],
+//				'artist_count' => ['callback' => function ($term_id) {
+//					$artists = get_posts([
+//						'post_type' => BASE . 'artist',
+//						'tax_query' => [[
+//							'taxonomy' => BASE . 'shop',
+//							'terms' => $term_id
+//						]],
+//						'posts_per_page' => -1,
+//						'fields' => 'ids'
+//					]);
+//					return count($artists);
+//				}],
+//			]
+//		],
+//
+//		'schema' => [
+//			'type' => 'LocalBusiness',
+//			'additional_types' => ['TattooParlor'],
+//			'properties' => [
+//				'address' => 'address',
+//				'telephone' => 'phone',
+//				'email' => 'email',
+//				'openingHours' => 'hours',
+//				'priceRange' => 'price_range',
+//				'image' => 'logo',
+//				'url' => ['callback' => function ($term_id) {
+//					$meta = Meta::forTerm($term_id);
+//					$website = $meta->get('website');
+//					return $website ?: get_term_link($term_id);
+//				}],
+//				'sameAs' => ['callback' => function ($term_id) {
+//					$meta = Meta::forTerm($term_id);
+//					$links = [];
+//					if ($ig = $meta->get('instagram')) $links[] = $ig;
+//					if ($fb = $meta->get('facebook')) $links[] = $fb;
+//					return !empty($links) ? $links : null;
+//				}],
+//				'memberOf' => [
+//					'@id' => 'https://edmonton.ink/#organization'
+//				],
+//			]
+//		]
+//	]);
+//
+//	// STYLE
+//	$taxonomies['style'] = array_merge($taxonomies['style'] ?? [], [
+//		'seo' => [
+//			'title_template' => 'Edmonton {{name}} Tattoo Artists | Specialists in {{name}}',
+//			'description_template' => '{{name}}{{alt_names}} is a distinctive tattoo style. {{characteristics}} Find Edmonton artists specializing in {{name}} tattoos.',
+//			'variables' => [
+//				'name' => 'term_name',
+//				'alt_names' => ['callback' => function ($term_id) {
+//					$meta = Meta::forTerm($term_id);
+//					$alts = $meta->get('alternate_name');
+//					if (!empty($alts) && is_array($alts)) {
+//						$names = array_filter(array_column($alts, 'name'));
+//						if (!empty($names)) {
+//							return ' (also known as ' . implode(', ', array_slice($names, 0, 2)) . ')';
+//						}
+//					}
+//					return '';
+//				}],
+//				'characteristics' => ['meta' => 'characteristics', 'truncate' => 100],
+//			]
+//		],
+//
+//		'schema' => [
+//			'type' => 'CreativeWork',
+//			'properties' => [
+//				'name' => ['callback' => function ($term_id) {
+//					return get_term($term_id)->name . ' Tattoo Style';
+//				}],
+//				'description' => 'characteristics',
+//				'about' => ['meta' => 'description'],
+//				'alternateName' => ['callback' => function ($term_id) {
+//					$meta = Meta::forTerm($term_id);
+//					$alts = $meta->get('alternate_name');
+//					if (!empty($alts) && is_array($alts)) {
+//						return array_filter(array_column($alts, 'name'));
+//					}
+//					return null;
+//				}],
+//			]
+//		]
+//	]);
+//
+//	// THEME
+//	$taxonomies['theme'] = array_merge($taxonomies['theme'] ?? [], [
+//		'seo' => [
+//			'title_template' => 'Edmonton {{name}} Tattoos | Find {{name}} Tattoo Designs',
+//			'description_template' => 'Explore {{name}} tattoos, a popular motif in Edmonton\'s tattoo scene. {{similar}}Find artists specializing in {{name}} designs.',
+//			'variables' => [
+//				'name' => 'term_name',
+//				'similar' => ['callback' => function ($term_id) {
+//					$meta = Meta::forTerm($term_id);
+//					$similar = $meta->get('similar');
+//					if (!empty($similar)) {
+//						$similar_names = [];
+//						foreach ((array)$similar as $similar_id) {
+//							$term = get_term($similar_id, BASE . 'theme');
+//							if ($term && !is_wp_error($term)) {
+//								$similar_names[] = html_entity_decode($term->name);
+//							}
+//						}
+//						if (!empty($similar_names)) {
+//							return 'Similar themes include ' . implode(', ', array_slice($similar_names, 0, 2)) . '. ';
+//						}
+//					}
+//					return '';
+//				}],
+//			]
+//		],
+//
+//		'schema' => [
+//			'type' => 'CreativeWork',
+//			'properties' => [
+//				'name' => ['callback' => function ($term_id) {
+//					return get_term($term_id)->name . ' Tattoo Theme';
+//				}],
+//				'description' => ['meta' => 'description'],
+//			]
+//		]
+//	]);
+//
+//	// CITY
+//	$taxonomies['city'] = array_merge($taxonomies['city'] ?? [], [
+//		'seo' => [
+//			'title_template' => '{{name}} Tattoo Artists & Shops | edmonton.ink',
+//			'description_template' => 'Discover {{name}}\'s vibrant tattoo scene featuring {{shop_count}} local shops and {{artist_count}} talented artists. Find top local talent and book your next tattoo today.',
+//			'variables' => [
+//				'name' => 'term_name',
+//				'shop_count' => ['callback' => function ($term_id) {
+//					$shops = get_terms([
+//						'taxonomy' => BASE . 'shop',
+//						'meta_key' => BASE . 'city',
+//						'meta_value' => $term_id,
+//						'fields' => 'count'
+//					]);
+//					return is_wp_error($shops) ? 0 : $shops;
+//				}],
+//				'artist_count' => ['callback' => function ($term_id) {
+//					$artists = get_posts([
+//						'post_type' => BASE . 'artist',
+//						'tax_query' => [[
+//							'taxonomy' => BASE . 'city',
+//							'terms' => $term_id
+//						]],
+//						'posts_per_page' => -1,
+//						'fields' => 'ids'
+//					]);
+//					return count($artists);
+//				}],
+//			]
+//		],
+//
+//		'schema' => [
+//			'type' => 'Place',
+//			'properties' => [
+//				'address' => ['callback' => function ($term_id) {
+//					$term = get_term($term_id);
+//					return [
+//						'@type' => 'PostalAddress',
+//						'addressLocality' => html_entity_decode($term->name),
+//						'addressRegion' => 'Alberta',
+//						'addressCountry' => 'CA'
+//					];
+//				}],
+//			]
+//		]
+//	]);
+//
+//	return $taxonomies;
+//});
diff --git a/inc/managers/SEO/render/SchemaOutput.php b/inc/managers/SEO/render/SchemaOutput.php
index d187f7d..6109e90 100644
--- a/inc/managers/SEO/render/SchemaOutput.php
+++ b/inc/managers/SEO/render/SchemaOutput.php
@@ -49,9 +49,24 @@
 			$registrar = Registrar::getInstance($type);
 			if ($registrar && !empty($registrar->getConfig('seo')['schema']??[])) {
 				$seo = $registrar->getSEO();
+				error_log('SEO: '.print_r($seo->schema(), true));
 				$schema[] = $seo->schema()->outputArchiveSchema();
 			}
 		}
+		$isContent = array_values(array_filter(array_map(function($item) {
+			return intval(get_option(BASE.$item.'_archive', false));
+		},Registrar::getFeatured('is_content', 'term'))));
+
+		if (is_page($isContent)){
+			$type = get_post_meta(get_the_id(), BASE.'for_type', true);
+			error_log('Type: '.print_r($type, true));
+			$registrar = Registrar::getInstance($type);
+			if ($registrar) {
+				$schema[] = $registrar->getSEO()->schema()->outputContentTaxArchiveSchema();
+			}
+		}
+
+
 
 		$breadcrumbs = $this->buildBreadcrumbs();
 		if ($breadcrumbs) {
@@ -61,7 +76,9 @@
 		if (!empty($schema)) {
 			$website = get_option(BASE.'WebsiteSchema');
 			if (!empty($website)) {
-				Cache::for('websiteSchema')->flush();
+				if (JVB_TESTING) {
+					Cache::for('websiteSchema')->flush();
+				}
 				$website = Cache::for('websiteSchema')->remember(
 					'schema',
 					function () {
@@ -104,6 +121,7 @@
 		echo '<script type="application/ld+json">' . "\n";
 		echo wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 		echo "\n" . '</script>' . "\n";
+		echo "\n" . '<!-- / SEO Schema by JakeVan -->'."\n";
 	}
 
 	public function buildBreadcrumbs():array
@@ -112,7 +130,11 @@
 	}
 
 	public function buildBasicWebsiteSchema():array
-	{	Cache::for('websiteSchema')->flush();
+	{
+		if (JVB_TESTING){
+			Cache::for('websiteSchema')->flush();
+		}
+
 		return Cache::for('websiteSchema')->remember(
 			'reference',
 			function () {
@@ -137,7 +159,10 @@
 
 	public function getCreator(bool $reference = false):LocalBusiness
 	{
-		Cache::for('JakeVanCreator')->flush();
+		if (JVB_TESTING){
+			Cache::for('JakeVanCreator')->flush();
+		}
+
 		if ($reference) {
 			return Cache::for('JakeVanCreator')->remember(
 				'reference',
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php b/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php
new file mode 100644
index 0000000..d77b11f
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php
@@ -0,0 +1,28 @@
+<?php
+namespace JVBase\managers\SEO\render\Thing\CreativeWork\VisualArtwork;
+
+use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
+use JVBase\managers\SEO\render\Traits\_Properties\artEditionTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\artformTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\artistTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\artMediumTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\artworkSurfaceTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\coloristTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\depthTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\heightTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\inkerTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\lettererTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\pencilerTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\weightTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\widthTrait;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class VisualArtwork extends CreativeWork
+{
+	use artEditionTrait, artMediumTrait, artFormTrait, artMediumTrait, artistTrait,
+		artworkSurfaceTrait, coloristTrait, depthTrait, heightTrait, inkerTrait, lettererTrait,
+		pencilerTrait, weightTrait, widthTrait;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php
new file mode 100644
index 0000000..50ef64a
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php
@@ -0,0 +1,2 @@
+<?php
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php');
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
index 5d7b483..b7922b9 100644
--- a/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
+++ b/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
@@ -1,5 +1,6 @@
 <?php
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/CreativeWork.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/DefinedTermSet.php');
 
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/CategoryCodeSet.php');
diff --git a/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php
new file mode 100644
index 0000000..30d517e
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php
@@ -0,0 +1,13 @@
+<?php
+namespace JVBase\managers\SEO\render\Thing\Place\AdministrativeArea;
+
+use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
+use JVBase\managers\SEO\render\Thing\Thing;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class City extends AdministrativeArea {
+
+}
diff --git a/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
index 1faa572..6965d2e 100644
--- a/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
+++ b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
@@ -1,3 +1,4 @@
 <?php
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/AdministrativeArea.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/Country.php');
diff --git a/inc/managers/SEO/render/Traits/ThingSchema.php b/inc/managers/SEO/render/Traits/ThingSchema.php
index e77d3e1..cfebbf6 100644
--- a/inc/managers/SEO/render/Traits/ThingSchema.php
+++ b/inc/managers/SEO/render/Traits/ThingSchema.php
@@ -31,13 +31,14 @@
 
 	protected array $ignore = [
 		'mappedMethods',
-		'ignore'
+		'ignore',
+		'id'
 	];
 	public function outputSchema():array
 	{
 		global $wp;
 		$current = home_url( add_query_arg( $_GET, $wp->request ) );
-		$id = (isset($this->id)) ? $this->id : $current.'/#'.strtolower($this->getTypeName());
+		$id = (isset($this->id)) ? $this->id : $current.'#'.strtolower($this->getTypeName());
 		$elements = array_map(
 			function ($value) {
 
@@ -118,4 +119,11 @@
 		}
 		$this->id = $id;
 	}
+
+	public function delete(string $property):void
+	{
+		if (property_exists($this, $property)) {
+			unset($this->$property);
+		}
+	}
 }
diff --git a/inc/managers/SEO/render/Traits/_Properties/_setup.php b/inc/managers/SEO/render/Traits/_Properties/_setup.php
index 7877c1d..5359535 100644
--- a/inc/managers/SEO/render/Traits/_Properties/_setup.php
+++ b/inc/managers/SEO/render/Traits/_Properties/_setup.php
@@ -28,6 +28,11 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/amountOfThisGoodTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/applicableCountryTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/areaServedTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artistTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artFormTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/associatedMediaTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/attendeeTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/audienceTrait.php');
@@ -78,6 +83,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/codeValueTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/colleagueTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/colorTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/coloristTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/companyRegistrationTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/composerTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/contactOptionTrait.php');
@@ -195,6 +201,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/imageTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/includesObjectTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/inCodeSetTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/inkerTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/inDefinedTermSetTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/ineligibleLocationTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/ineligibleRegionTrait.php');
@@ -234,6 +241,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/legalAddressTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/legalNameTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/legalRepresentativeTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/lettererTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/licenseTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/lifeEventTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/lineTrait.php');
@@ -294,6 +302,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/patternTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/paymentAcceptedTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/paymentMethodTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/performerInTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/performerTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/performTimeTrait.php');
diff --git a/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php b/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php
index 633666a..4ef2753 100644
--- a/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php
@@ -2,6 +2,7 @@
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
 use JVBase\managers\SEO\render\Thing\Thing;
+use JVBase\registrar\config\seo\Resolver;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -17,8 +18,34 @@
 	{
 		return $this->about??null;
 	}
-	public function setAbout(Thing $about):void
+	public function setAbout(Thing|array $about):void
 	{
+		if (is_array($about)) {
+			if (!array_key_exists('type', $about)) {
+				error_log('[aboutTrait] Needs to have a type key'.print_r($about, true));
+				return;
+			}
+			if (!class_exists($about['type'])) {
+				error_log('[aboutTrait] Class not found for: '.print_r($about['type'], true));
+				return;
+			}
+			$class = new $about['type']();
+			$className = $about['type'];
+			unset($about['type']);
+			foreach ($about as $key => $value) {
+				$method = 'set'.ucfirst($key);
+				if (method_exists($class, $method)) {
+					if (is_string($value) && str_contains($value, '{{')) {
+						$value = Resolver::resolveForSchema($key, $value, $class);
+					}
+					error_log('Setting '.$key.' to '.print_r($value, true));
+					$class->$method($value);
+				} else {
+					error_log('[aboutTrait] Invalid method: '.$method.', for class: '.$className);
+				}
+			}
+			$about = $class;
+		}
 		$this->about = $about;
 	}
 }
diff --git a/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php b/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php
new file mode 100644
index 0000000..c240cc8
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php
@@ -0,0 +1,21 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Properties;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait artEditionTrait {
+	/**
+	 * @var int|string The number of copies when multiple copies of a piece of artwork are produced - e.g. for a limited edition of 20 prints, 'artEdition' refers to the total number of copies (in this example "20").
+	 */
+	protected int|string $artEdition;
+
+	public function getArtEdition():int|string|null
+	{
+		return $this->artEdition??null;
+	}
+	public function setArtEdition(int|string $artEdition):void
+	{
+		$this->artEdition = $artEdition;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php b/inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php
new file mode 100644
index 0000000..a53ca16
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php
@@ -0,0 +1,21 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Properties;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait artMediumTrait {
+	/**
+	 * @var string The material used. (E.g. Oil, Watercolour, Acrylic, Linoprint, Marble, Cyanotype, Digital, Lithograph, DryPoint, Intaglio, Pastel, Woodcut, Pencil, Mixed Media, etc.)
+	 */
+	protected string $artMedium;
+
+	public function getArtMedium():?string
+	{
+		return $this->artMedium??null;
+	}
+	public function setArtMedium(string $artMedium):void
+	{
+		$this->artMedium = $artMedium;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/artformTrait.php b/inc/managers/SEO/render/Traits/_Properties/artformTrait.php
new file mode 100644
index 0000000..24b05b5
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artformTrait.php
@@ -0,0 +1,21 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Properties;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait artformTrait {
+	/**
+	 * @var string e.g. Painting, Drawing, Sculpture, Print, Photograph, Assemblage, Collage, etc.
+	 */
+	protected string $artform;
+
+	public function getArtform():?string
+	{
+		return $this->artform??null;
+	}
+	public function setArtform(string $artform):void
+	{
+		$this->artform = $artform;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/artistTrait.php b/inc/managers/SEO/render/Traits/_Properties/artistTrait.php
new file mode 100644
index 0000000..b98e907
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artistTrait.php
@@ -0,0 +1,23 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Properties;
+
+use JVBase\managers\SEO\render\Thing\Person\Person;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait artistTrait {
+	/**
+	 * @var Person The primary artist for a work in a medium other than pencils or digital line art--for example, if the primary artwork is done in watercolors or digital paints.
+	 */
+	protected Person $artist;
+
+	public function getArtist():?Person
+	{
+		return $this->artist??null;
+	}
+	public function setArtist(Person $artist):void
+	{
+		$this->artist = $artist;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php b/inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php
new file mode 100644
index 0000000..da7413f
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php
@@ -0,0 +1,21 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Properties;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait artworkSurfaceTrait {
+	/**
+	 * @var string The supporting materials for the artwork, e.g. Canvas, Paper, Wood, Board, etc. Supersedes surface.
+	 */
+	protected string $artworkSurface;
+
+	public function getArtworkSurface():?string
+	{
+		return $this->artworkSurface??null;
+	}
+	public function setArtworkSurface(string $artworkSurface):void
+	{
+		$this->artworkSurface = $artworkSurface;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/coloristTrait.php b/inc/managers/SEO/render/Traits/_Properties/coloristTrait.php
new file mode 100644
index 0000000..bea9627
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/coloristTrait.php
@@ -0,0 +1,23 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Properties;
+
+use JVBase\managers\SEO\render\Thing\Person\Person;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait coloristTrait {
+	/**
+	 * @var Person The individual who adds color to inked drawings.
+	 */
+	protected Person $colorist;
+
+	public function getColorist():?Person
+	{
+		return $this->colorist??null;
+	}
+	public function setColorist(Person $colorist):void
+	{
+		$this->colorist = $colorist;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/creatorTrait.php b/inc/managers/SEO/render/Traits/_Properties/creatorTrait.php
index 738e5b7..30b69cc 100644
--- a/inc/managers/SEO/render/Traits/_Properties/creatorTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/creatorTrait.php
@@ -4,6 +4,7 @@
 use JVBase\managers\SEO\render\Thing\Organization\Organization;
 use JVBase\managers\SEO\render\Thing\Person\Person;
 use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
+use JVBase\registrar\config\seo\Resolver;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -19,8 +20,16 @@
 	{
 		return $this->creator??null;
 	}
-	public function setCreator(Organization|Person|array $creator):void
+	public function setCreator(string|Organization|Person|array $creator):void
 	{
+		if (is_string($creator)) {
+			if (empty($creator)) {
+				return;
+			}
+			error_log('Creator value: '.print_r($creator, true));
+			//TODO: Set creator from string
+			return;
+		}
 		if (is_array($creator)) {
 			$creator = $this->mixedArray('creator', $creator, [
 				'JVBase\managers\SEO\render\Thing\Organization\Organization',
diff --git a/inc/managers/SEO/render/Traits/_Properties/inkerTrait.php b/inc/managers/SEO/render/Traits/_Properties/inkerTrait.php
new file mode 100644
index 0000000..4dfa2b0
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/inkerTrait.php
@@ -0,0 +1,23 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Properties;
+
+use JVBase\managers\SEO\render\Thing\Person\Person;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait inkerTrait {
+	/**
+	 * @var Person The individual who traces over the pencil drawings in ink after pencils are complete.
+	 */
+	protected Person $inker;
+
+	public function getInker():?Person
+	{
+		return $this->inker??null;
+	}
+	public function setInker(Person $inker):void
+	{
+		$this->inker = $inker;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/lettererTrait.php b/inc/managers/SEO/render/Traits/_Properties/lettererTrait.php
new file mode 100644
index 0000000..e963913
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/lettererTrait.php
@@ -0,0 +1,23 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Properties;
+
+use JVBase\managers\SEO\render\Thing\Person\Person;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait lettererTrait {
+	/**
+	 * @var Person The individual who adds lettering, including speech balloons and sound effects, to artwork.
+	 */
+	protected Person $letterer;
+
+	public function getLetterer():?Person
+	{
+		return $this->letterer??null;
+	}
+	public function setLetterer(Person $letterer):void
+	{
+		$this->letterer = $letterer;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php b/inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php
new file mode 100644
index 0000000..ed9a76d
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php
@@ -0,0 +1,23 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Properties;
+
+use JVBase\managers\SEO\render\Thing\Person\Person;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait pencilerTrait {
+	/**
+	 * @var Person The individual who draws the primary narrative artwork.
+	 */
+	protected Person $penciler;
+
+	public function getPenciler():?Person
+	{
+		return $this->penciler??null;
+	}
+	public function setPenciler(Person $penciler):void
+	{
+		$this->penciler = $penciler;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php b/inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php
index 28cf23f..4cb72f9 100644
--- a/inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php
@@ -16,8 +16,15 @@
 	{
 		return $this->sourceOrganization??null;
 	}
-	public function setSourceOrganization(Organization $sourceOrganization):void
+	public function setSourceOrganization(string|Organization $sourceOrganization):void
 	{
+		if (is_string($sourceOrganization)) {
+			if (empty($sourceOrganization)) {
+				return;
+			}
+			//TODO: create organization from string
+			return;
+		}
 		$this->sourceOrganization = $sourceOrganization;
 	}
 }
diff --git a/inc/managers/ScriptLoader.php b/inc/managers/ScriptLoader.php
index 6418d9c..838644e 100644
--- a/inc/managers/ScriptLoader.php
+++ b/inc/managers/ScriptLoader.php
@@ -44,6 +44,7 @@
 		$version,
 		$strategy
 	);
+	wp_localize_script('jvb-auth', 'jvbBase', ['base' => BASE]);
 	wp_register_script(
 		'jvb-interactions',
 		JVB_URL.'assets/js/min/interactions.min.js',
@@ -212,6 +213,7 @@
 		$strategy
 	);
 
+
 	//SEO Admin
 	wp_register_script(
 		'jvb-schema',
diff --git a/inc/meta/Form.php b/inc/meta/Form.php
index 57bdd85..0af4d26 100644
--- a/inc/meta/Form.php
+++ b/inc/meta/Form.php
@@ -1179,7 +1179,7 @@
 			'type'		=> $config['subtype'],
 		], $config);
 
-		$registrar = Registrar::getInstance($config['subtype']);
+		$registrar = Registrar::getInstance($config[$config['subtype']]);
 		$icon = jvbDefaultIcon();
 		if ($registrar){
 			$icon = $registrar->getIcon()??jvbDefaultIcon();
@@ -1597,8 +1597,8 @@
 	{
 		$fields = $config['fields'] ?? [];
 		$rows = is_array($value) ? $value : [];
-		if(array_key_exists('row_label', $config)) {
-			$config['data']['label'] = esc_attr($config['row_label']);
+		if(array_key_exists('add_label', $config)) {
+			$config['data']['label'] = esc_attr($config['add_label']);
 		}
 
 		$input = sprintf(
diff --git a/inc/meta/Item.php b/inc/meta/Item.php
index 65e9ecf..ff1fded 100644
--- a/inc/meta/Item.php
+++ b/inc/meta/Item.php
@@ -47,7 +47,7 @@
 			'user_email',
 		],
 		'term' => [
-			'term_name',
+			'name',
 			'description'
 		]
 	];
diff --git a/inc/meta/MetaFormOld.php b/inc/meta/MetaFormOld.php
index 871e771..ddfd94f 100644
--- a/inc/meta/MetaFormOld.php
+++ b/inc/meta/MetaFormOld.php
@@ -642,7 +642,7 @@
 		$values = is_array($value) ? $value : array();
 
 		$conditional = $this->handleConditionalField($field);
-		$row_label = isset($field['row_label']) ? $field['row_label'] : '';
+		$row_label = isset($field['add_label']) ? $field['add_label'] : '';
 		$rowTitle = (array_key_exists('new_row', $field)) ? $field['new_row'] : 'New Item';
 		if (array_key_exists('group', $field)) {
 			$name = $field['group'].'::'.$name;
diff --git a/inc/meta/MetaTypeManager.php b/inc/meta/MetaTypeManager.php
index 69a2fd9..e0da68f 100644
--- a/inc/meta/MetaTypeManager.php
+++ b/inc/meta/MetaTypeManager.php
@@ -85,12 +85,17 @@
             'sanitize'  => 'sanitizeUser',
 			'default'	=> '',
         ],
+        'post'  => [
+            'type'      => 'string',
+            'sanitize'  => 'sanitizePost',
+			'default'	=> '',
+        ],
         'repeater' => [
             'type' => 'object',
             'sanitize' => 'sanitizeRepeater',
 			'default'	=> [],
         ],
-		'tag_list' => [
+		'taglist' => [
 			'type' => 'object',
 			'sanitize' => 'sanitizeTagList',
 			'default' => []
@@ -133,7 +138,12 @@
             'type'  => 'string',
             'sanitize'  => 'sanitize_text_field',
 			'default'	=> '',
-        ]
+        ],
+		'selector' => [
+			'type'	=> 'string',
+			'sanitize'	=> 'sanitizeSelector',
+			'default'	=> '',
+		]
     ];
 	public static function getType(string $field_name):array
 	{
diff --git a/inc/meta/Repeater.php b/inc/meta/Repeater.php
index 583ae44..ae1622e 100644
--- a/inc/meta/Repeater.php
+++ b/inc/meta/Repeater.php
@@ -392,6 +392,6 @@
 	 */
 	public function rowLabelField(): ?string
 	{
-		return $this->config['row_label'] ?? null;
+		return $this->config['add_label'] ?? null;
 	}
 }
diff --git a/inc/meta/Sanitizer.php b/inc/meta/Sanitizer.php
index e6cad1c..869065d 100644
--- a/inc/meta/Sanitizer.php
+++ b/inc/meta/Sanitizer.php
@@ -31,11 +31,9 @@
            MetaTypeManager::getSanitizeCallback($field_config['type']);
     }
 
-    protected static function sanitizeTaxonomy(array|string $values, array $field_config):string
+    protected static function sanitizeTaxonomy(string $values, array $field_config):string
     {
-        if (!is_array($values)) {
-            $values = explode(',', $values);
-        }
+		$values = array_map('absint', explode(',', $values));
 
         // Ensure taxonomy starts with BASE
         $taxonomy = (str_starts_with($field_config['taxonomy'], BASE))
@@ -47,17 +45,21 @@
         return implode(',', $values);
     }
 
-    protected static function sanitizeUser(array|string $values, array $field_config):string
+    protected static function sanitizeUser(string $values, array $field_config):string
     {
-        if (!is_array($values)) {
-            $values = explode(',', $values);
-        }
+		$values = array_map('absint', explode(',', $values));
 
         $values = array_filter($values, fn($value) => (bool)get_userdata((int)$value));
 
         return implode(',', $values);
     }
 
+	protected static function sanitizePost(string $values, array $config):string
+	{
+		$values = array_map('absint', explode(',', $values));
+		return implode(',', array_filter($values, fn($value) => (bool)get_post((int)$value)));
+	}
+
 	protected static function sanitizeTagList(array $values, array $field_config): array
 	{
 		if (empty(array_filter($values, fn($value) => !empty($value)))) {
@@ -171,6 +173,18 @@
 		return $sanitized;
 	}
 
+	protected static function sanitizeSelector(string $value, array $config):string
+	{
+		if (array_key_exists('type', $config)) {
+			return match ($config['type']) {
+				'user' => self::sanitizeUser($value, $config),
+				'taxonomy'=> self::sanitizeTaxonomy($value, $config),
+				'post'	=> self::sanitizePost($value, $config),
+			};
+		}
+		return implode(',',array_map('absint', explode(',',$value)));
+	}
+
     protected static function sanitizeUpload(array|string $value):string
     {
         if (empty($value)) {
diff --git a/inc/meta/Storage.php b/inc/meta/Storage.php
index 74538ff..fd0f927 100644
--- a/inc/meta/Storage.php
+++ b/inc/meta/Storage.php
@@ -440,7 +440,7 @@
 	protected function getTermField(Item $item, string $name): mixed
 	{
 		return match ($name) {
-			'term_name' => get_term_field('name', $item->id),
+			'name' => get_term_field('name', $item->id),
 			'description' => get_term_field('description', $item->id),
 			default => ''
 		};
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index 47f9f55..7386cd7 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -4,8 +4,10 @@
 use JVBase\registrar\fields\Field;
 use JVBase\registrar\fields\GroupedField;
 use JVBase\registrar\fields\OptionsField;
-use JVBase\registrar\fields\TaxonomyField;
-use JVBase\registrar\fields\Upload;
+use JVBase\registrar\fields\RepeaterField;
+use JVBase\registrar\fields\TagListField;
+use JVBase\registrar\fields\SelectorField;
+use JVBase\registrar\fields\UploadField;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -16,6 +18,7 @@
 	protected Registrar $registrar;
 
 	public function __construct(?string $type = null, ?Registrar $registrar = null) {
+		$this->registrar = $registrar;
 		switch ($type) {
 			case 'post':
 				$this->addPostFields();
@@ -27,15 +30,16 @@
 				$this->addUserFields();
 				break;
 		}
-		$this->registrar = $registrar;
 	}
 
 	public function addField(string $name, array $config):self {
 		$this->fields[$name] = match ($config['type']) {
-			'upload', 'image', 'gallery' 	=> new Upload($name, $config),
+			'upload', 'image', 'gallery' 	=> new UploadField($name, $config),
 			'checkbox', 'radio', 'select', 'set' => new OptionsField($name, $config),
-			'repeater', 'group', 'tagList' => new GroupedField($name, $config),
-			'selector', 'taxonomy', 'user', 'post' => new TaxonomyField($name, $config),
+			'group' => new GroupedField($name, $config),
+			'repeater'	=> new RepeaterField($name, $config),
+			'tagList'	=> new TagListField($name, $config),
+			'selector', 'taxonomy', 'user', 'post' => new SelectorField($name, $config),
 			default => new Field($name, $config),
 		};
 
@@ -117,10 +121,10 @@
 				'label'		=> 'Description',
 			]
 		];
-		if ($this->registrar->registrar->hierarchical){
+		if ($this->registrar->args()['hierarchical']??false && $this->registrar->args()['hierarchical'] === true){
 			$fields['parent'] = [
 				'type'			=> 'taxonomy',
-				'taxonomy_type'	=> 'reference',
+				'isReference'	=> true,
 				'autocomplete'	=> true,
 				'label'			=> 'Term Parent'
 			];
@@ -170,4 +174,182 @@
 	{
 		return $this->fields;
 	}
+
+	public function addCommon(string $name):self
+	{
+		match ($name) {
+			'wiki'		=> $this->addWikiField(),
+			'links'		=> $this->addLinksField(),
+			'contact'	=> $this->addContactField(),
+			'reviews', 'review'	=> $this->addReviewField(),
+			'alternate_name'	=> $this->addAlternateName(),
+			'keywords' => $this->addKeywords(),
+			default => error_log('[Field]addCommon: No configuration found for '.$name.'.')
+		};
+		return $this;
+	}
+
+	protected function addWikiField():void
+	{
+		$this->addField(
+			'wiki',
+			[
+				'type'        => 'url',
+				'label'       => 'Wikipedia Page',
+				'description' => 'For the schema',
+				'quickEdit'   => true,
+			]
+		);
+	}
+
+	protected function addLinksField():void
+	{
+		$this->addField(
+			'links',
+			[
+				'type'        => 'repeater',
+				'quickEdit'   => true,
+				'add_label'	=> 'title',
+				'label'       => 'Online Links',
+				'description' => 'These are listed publicly on the website',
+				'fields'      => [
+					'url'     => [
+						'type'  => 'url',
+						'label' => 'URL',
+					],
+					'title'   => [
+						'type'  => 'text',
+						'label' => 'Label',
+					],
+					'tracker' => [
+						'type'        => 'text',
+						'label'       => 'Tracker',
+						'description' => 'If you are set up to track link referrals, add what comes after the ? here.',
+					],
+				],
+				'section'     => 'contact'
+			]
+		);
+	}
+
+	protected function addContactField():void
+	{
+		$this->addField(
+			'admin_contact',
+			[
+				'type'      => 'set',
+				'label'     => 'Admin Contact',
+				'quickEdit' => true,
+				'options'   => [
+					'text'  => 'Text',
+					'call'  => 'Call',
+					'email' => 'Email',
+					'insta' => 'Instagram',
+				],
+				'section'   => 'contact'
+			]
+		);
+		$this->addField(
+			'public_contact',
+			[
+				'type'      => 'set',
+				'label'     => 'Public Contact',
+				'quickEdit' => true,
+				'options'   => [
+					'text'  => 'Text',
+					'call'  => 'Call',
+					'email' => 'Email',
+					'insta' => 'Instagram',
+				],
+				'section'   => 'contact'
+			]
+		);
+	}
+
+	protected function addReviewField():void
+	{
+		$this->addField(
+			'reviews',
+			[
+				'type'    => 'repeater',
+				'add_label'	=> 'name',
+				'label'   => 'Reviews',
+				'fields'  => [
+					'name'   => [
+						'type'  => 'text',
+						'label' => 'Reviewer Name',
+					],
+					'review' => [
+						'type'  => 'textarea',
+						'quill' => false,
+						'label' => 'Review',
+					],
+					'rating' => [
+						'type'    => 'select',
+						'label'   => 'Rating',
+						'options' => [
+							'none' => 'Not Given',
+							'0.5'  => '0.5',
+							'1'    => '1',
+							'1.5'  => '1.5',
+							'2'    => '2',
+							'2.5'  => '2.5',
+							'3'    => '3',
+							'3.5'  => '3.5',
+							'4'    => '4',
+							'4.5'  => '4.5',
+							'5'    => '5',
+						],
+						'default' => 'none'
+					],
+					'date'   => [
+						'type'  => 'date',
+						'label' => 'Date of Review',
+					],
+					'url'    => [
+						'type'  => 'url',
+						'label' => 'Link to Review (optional)',
+					],
+				],
+				'section' => 'seo'
+			]
+		);
+	}
+
+	protected function addAlternateName():void
+	{
+		$this->addField(
+			'alternate_name',
+			[
+				'type'    => 'repeater',
+				'label'   => 'Alternate Name',
+				'fields'  => [
+					'name' => [
+						'type'  => 'text',
+						'label' => 'Name',
+					]
+				],
+				'section' => 'seo'
+			]
+		);
+	}
+	protected function addKeywords():void
+	{
+		$this->addField(
+			'keywords',
+			[
+				'type'      => 'repeater',
+				'label'     => 'Keywords',
+				'fields'    => [
+					'keyword' => [
+						'type'  => 'text',
+						'label' => 'Keyword',
+					],
+				],
+				'default'   => $labels ?? [ 'Edmonton tattoos', 'Edmonton tattoo artist', 'Edmonton tattooist' ],
+				'section'   => 'seo',
+				'quickEdit' => true,
+			]
+		);
+	}
 }
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index ad174ca..33a8687 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -1,8 +1,10 @@
 <?php
 namespace JVBase\registrar;
 
+use JVBase\managers\Cache;
 use JVBase\managers\CRUD;
 use JVBase\managers\IconsManager;
+use JVBase\meta\Meta;
 use JVBase\registrar\config\Breadcrumbs;
 use JVBase\registrar\config\Dashboard;
 use JVBase\registrar\config\Directory;
@@ -15,6 +17,7 @@
 use JVBase\registrar\helpers\MakeTrackChanges;
 use JVBase\registrar\helpers\MakeVerification;
 use JVBase\utility\Features;
+use WP_Query;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -36,6 +39,8 @@
 
 	protected ?string $upload_title = null;
 
+	protected int|false $page = false;
+
 	protected static array $allFlags = [
 		//Shared Flags
 		'favouritable', 'karma', 'show_feed', 'show_directory', 'approve_new', 'has_responses', 'invitable',
@@ -281,6 +286,11 @@
 		return $this->fields;
 	}
 
+	public function args():array
+	{
+		return $this->args;
+	}
+
 	public function setFields():void
 	{
 		$this->fields = new Fields($this->type, $this);
@@ -401,6 +411,11 @@
 		});
 		foreach ($flags as $flag) {
 			$this->$flag = true;
+			switch ($flag) {
+				case 'is_content':
+					add_action('init', [$this, 'setupContent'], 20);
+					break;
+			}
 		}
 		return $this;
 	}
@@ -639,4 +654,106 @@
 
 		return $content;
 	}
+
+	public function setupContent():void
+	{
+		if (!$this->is_content) return;
+		//We need a pseudo-archive page for this content taxonomy. We create a post with the plural name
+		$this->page = get_option(BASE.$this->slug.'_archive', false);
+		if (!$this->page || !(bool)get_post((int)$this->page)) {
+			$exists = new WP_Query([
+				'post_type'	=> 'page',
+				'title'=> $this->plural,
+				'posts_per_page'	=> 1,
+				'post_status'	=> 'publish',
+				'fields'	=> 'ids',
+			]);
+			if ($exists->have_posts()) {
+				$page = $exists->posts[0];
+			} else {
+				$page = wp_insert_post([
+					'post_type'		=> 'page',
+					'post_title'	=> $this->plural,
+					'post_content'	=> '',
+					'post_status'	=> 'publish',
+				]);
+			}
+
+			if ($page && !is_wp_error($page)) {
+				update_post_meta($page, BASE.'for_type', $this->slug);
+				update_option(BASE.$this->slug.'_archive', $page);
+				$this->page = $page;
+			}
+			wp_reset_postdata();
+		}
+
+		add_filter('jvb_post_content_output', [$this, 'renderContent'], 20, 2);
+	}
+	public function renderContent(string $content, array $block):string
+	{
+		if (!is_page($this->page)) {
+			return $content;
+		}
+		if ($block['blockName'] !== 'core/post-content') {
+			return $content;
+		}
+		if (JVB_TESTING) {
+			Cache::for($this->slug)->flush();
+		}
+
+		$out = Cache::for($this->slug)->remember(
+			get_the_ID(),
+			function() {
+
+				$items = get_terms([
+					'taxonomy'	=> jvbCheckBase($this->slug),
+//						'hide_empty' => true,
+					'fields'	=> 'ids',
+				]);
+				$out = [];
+				if ($items && !is_wp_error($items)) {
+					foreach ($items as $item) {
+						$meta = Meta::forTerm($item);
+						$slug = sanitize_title($meta->get('name'));
+						$item = sprintf(
+							'<li id="%s"><h3><a href="%s">%s</a></h3><p>%s</p><ul>',
+							$slug,
+							get_term_link($item, jvbCheckBase($this->slug))??'',
+							$meta->get('name'),
+							$meta->get('description')
+						);
+							$postTypes = array_map(function($type) { return jvbCheckBase($type);}, $this->registrar->for);
+							$posts = new WP_Query([
+								'post_type'		=> $postTypes,
+								'post_status'	=> 'publish',
+								'posts_per_page'	=> 3,
+								'fields'	=> 'ids',
+							]);
+							if ($posts->have_posts()) {
+								while($posts->have_posts()) {
+									$posts->the_post();
+									$ID = get_the_id();
+									$postMeta = Meta::forPost($ID);
+									$img = $postMeta->get('post_thumbnail');
+									$img = !empty($img) ? jvbFormatImage((int)$img, 'tiny', 'medium') : '';
+									$item .= sprintf(
+										'<li id="%s"><h4><a href="%s">%s</a></h4>%s</li>',
+										$slug.'-'.sanitize_title(get_the_title($ID)),
+										get_the_permalink($ID),
+										$postMeta->get('post_title'),
+										$img
+									);
+								}
+							}
+							wp_reset_postdata();
+						$item .= '</ul></li>';
+						$out[] = $item;
+					}
+				}
+				return empty($out) ? '' : '<ul class="content-term-list">'.implode('',$out).'</ul>';
+			}
+		);
+		error_log('Built the '.$this->slug.' page content.');
+		return $content . $out;
+	}
 }
diff --git a/inc/registrar/Terms.php b/inc/registrar/Terms.php
index 20890dd..e1b875e 100644
--- a/inc/registrar/Terms.php
+++ b/inc/registrar/Terms.php
@@ -179,75 +179,78 @@
 
 	public function register():void
 	{
-		$args = [
-			'labels'		=> $this->labels,
-			'public'		=> $this->public,
-			'hierarchical'	=> $this->hierarchical,
-		];
-			if (isset($this->description)) {
-				$args['description'] = $this->description;
-			}
-			if (isset($this->publicly_queryable)) {
-				$args['publicly_queryable'] = $this->publicly_queryable;
-			}
-			if (isset($this->show_ui)) {
-				$args['show_ui'] = $this->show_ui;
-			}
-			if (isset($this->show_in_menu)) {
-				$args['show_in_menu'] = $this->show_in_menu;
-			}
-			if (isset($this->show_in_nav_menus)) {
-				$args['show_in_nav_menus'] = $this->show_in_nav_menus;
-			}
-			if (isset($this->show_in_rest)) {
-				$args['show_in_rest'] = $this->show_in_rest;
-			}
-			if (isset($this->rest_base)) {
-				$args['rest_base'] = $this->rest_base;
-			}
-			if (isset($this->rest_namespace)) {
-				$args['rest_namespace'] = $this->rest_namespace;
-			}
-			if (isset($this->rest_controller_class)) {
-				$args['rest_controller_class'] = $this->rest_controller_class;
-			}
-			if (isset($this->show_tag_cloud)) {
-				$args['show_tag_cloud'] = $this->show_tag_cloud;
-			}
-			if (isset($this->show_quick_edit)) {
-				$args['show_quick_edit'] = $this->show_quick_edit;
-			}
-			if (isset($this->show_admin_column)) {
-				$args['show_admin_column'] = $this->show_admin_column;
-			}
-			if (isset($this->meta_box_cb) && is_callable($this->meta_box_cb)) {
-				$args['meta_box_cb'] = $this->meta_box_cb;
-			}
-			if (isset($this->meta_box_sanitize_cb) && is_callable($this->meta_box_sanitize_cb)) {
-				$args['meta_box_sanitize_cb'] = $this->meta_box_sanitize_cb;
-			}
-			if (isset($this->capabilities)) {
-				$allowed = ['manage_terms', 'edit_terms', 'delete_terms', 'assign_terms'];
-				$caps = array_filter($this->capabilities, function ($cap) use ($allowed) {
-					return in_array($cap, $allowed);
-				}, ARRAY_FILTER_USE_KEY);
-				$args['capabilities'] = $caps;
-			}
-			if (isset($this->query_var)) {
-				$args['query_var'] = $this->query_var;
-			}
-			if (isset($this->update_count_callback) && is_callable($this->update_count_callback)) {
-				$args['update_count_callback'] = $this->update_count_callback;
-			}
-			if (isset($this->default_term)) {
-				$args['default_term'] = $this->default_term;
-			}
-			if (isset($this->sort)) {
-				$args['sort'] = $this->sort;
-			}
-			if (isset($this->args)) {
-				$args['args'] = $this->args;
-			}
+
+		$args = array_filter(get_object_vars($this));
+//		$args = [
+//			'labels'		=> $this->labels,
+//			'public'		=> $this->public,
+//			'hierarchical'	=> $this->hierarchical,
+//		];
+//			if (isset($this->description)) {
+//				$args['description'] = $this->description;
+//			}
+//			if (isset($this->publicly_queryable)) {
+//				$args['publicly_queryable'] = $this->publicly_queryable;
+//			}
+//			if (isset($this->show_ui)) {
+//				$args['show_ui'] = $this->show_ui;
+//			}
+//			if (isset($this->show_in_menu)) {
+//				$args['show_in_menu'] = $this->show_in_menu;
+//			}
+//			if (isset($this->show_in_nav_menus)) {
+//				$args['show_in_nav_menus'] = $this->show_in_nav_menus;
+//			}
+//			if (isset($this->show_in_rest)) {
+//				$args['show_in_rest'] = $this->show_in_rest;
+//			}
+//			if (isset($this->rest_base)) {
+//				$args['rest_base'] = $this->rest_base;
+//			}
+//			if (isset($this->rest_namespace)) {
+//				$args['rest_namespace'] = $this->rest_namespace;
+//			}
+//			if (isset($this->rest_controller_class)) {
+//				$args['rest_controller_class'] = $this->rest_controller_class;
+//			}
+//			if (isset($this->show_tag_cloud)) {
+//				$args['show_tag_cloud'] = $this->show_tag_cloud;
+//			}
+//			if (isset($this->show_quick_edit)) {
+//				$args['show_quick_edit'] = $this->show_quick_edit;
+//			}
+//			if (isset($this->show_admin_column)) {
+//				$args['show_admin_column'] = $this->show_admin_column;
+//			}
+//			if (isset($this->meta_box_cb) && is_callable($this->meta_box_cb)) {
+//				$args['meta_box_cb'] = $this->meta_box_cb;
+//			}
+//			if (isset($this->meta_box_sanitize_cb) && is_callable($this->meta_box_sanitize_cb)) {
+//				$args['meta_box_sanitize_cb'] = $this->meta_box_sanitize_cb;
+//			}
+//			if (isset($this->capabilities)) {
+//				$allowed = ['manage_terms', 'edit_terms', 'delete_terms', 'assign_terms'];
+//				$caps = array_filter($this->capabilities, function ($cap) use ($allowed) {
+//					return in_array($cap, $allowed);
+//				}, ARRAY_FILTER_USE_KEY);
+//				$args['capabilities'] = $caps;
+//			}
+//			if (isset($this->query_var)) {
+//				$args['query_var'] = $this->query_var;
+//			}
+//			if (isset($this->update_count_callback) && is_callable($this->update_count_callback)) {
+//				$args['update_count_callback'] = $this->update_count_callback;
+//			}
+//			if (isset($this->default_term)) {
+//				$args['default_term'] = $this->default_term;
+//			}
+//			if (isset($this->sort)) {
+//				$args['sort'] = $this->sort;
+//			}
+//			if (isset($this->args)) {
+//				$args['args'] = $this->args;
+//			}
+		unset($args['for']);
 
 		$for = array_map(function($item) { return jvbCheckBase($item);}, $this->for);
 		register_taxonomy(jvbCheckBase($this->taxonomy), $for, $args);
diff --git a/inc/registrar/_setup.php b/inc/registrar/_setup.php
index a6eddc2..897eb11 100644
--- a/inc/registrar/_setup.php
+++ b/inc/registrar/_setup.php
@@ -9,9 +9,11 @@
 
 require(JVB_DIR . '/inc/registrar/fields/Field.php');
 require(JVB_DIR . '/inc/registrar/fields/GroupedField.php');
+require(JVB_DIR . '/inc/registrar/fields/RepeaterField.php');
+require(JVB_DIR . '/inc/registrar/fields/TagListField.php');
 require(JVB_DIR . '/inc/registrar/fields/OptionsField.php');
-require(JVB_DIR . '/inc/registrar/fields/TaxonomyField.php');
-require(JVB_DIR . '/inc/registrar/fields/Upload.php');
+require(JVB_DIR . '/inc/registrar/fields/SelectorField.php');
+require(JVB_DIR . '/inc/registrar/fields/UploadField.php');
 
 require(JVB_DIR . '/inc/registrar/helpers/MakeCalendarType.php');
 require(JVB_DIR . '/inc/registrar/helpers/MakeTrackChanges.php');
diff --git a/inc/registrar/config/SEO.php b/inc/registrar/config/SEO.php
index 7617b7b..4797cf9 100644
--- a/inc/registrar/config/SEO.php
+++ b/inc/registrar/config/SEO.php
@@ -33,7 +33,24 @@
 
 	protected function initSchema():void
 	{
-		if (!array_key_exists('type', $this->config['schema'])){
+		if (!array_key_exists('type', $this->config['schema'])) {
+			$registrar = Registrar::getInstance($this->slug);
+			if ($registrar) {
+				switch ($registrar->getType()) {
+					case 'term':
+						$this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
+						break;
+					case 'post':
+						$this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork';
+						break;
+					case 'user':
+						$this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ProfilePage';
+						break;
+				}
+
+			}
+		}
+		if (!array_key_exists('type', $this->config['schema'])) {
 			error_log('Missing schema type');
 			return;
 		}
diff --git a/inc/registrar/config/seo/Helpers.php b/inc/registrar/config/seo/Helpers.php
index ce4ae53..c5f0f5f 100644
--- a/inc/registrar/config/seo/Helpers.php
+++ b/inc/registrar/config/seo/Helpers.php
@@ -9,11 +9,12 @@
 function jvbTSFDoIt(string $slug, ?array $args):bool
 {
 	$tsf = tsf();
+
 	return (null === $args)
 		? $tsf->query()->is_singular()
-		&& BASE.$slug === $tsf->query()->get_current_post_type()
+		&& jvbCheckBase($slug) === $tsf->query()->get_current_post_type()
 		: 'single' === The_SEO_Framework\get_query_type_from_args( $args )
-		&& BASE.$slug === $tsf->query()->get_post_type_real_id( $args['id'] );
+		&& jvbCheckBase($slug) === $tsf->query()->get_post_type_real_id( $args['id'] );
 }
 
 function jvbTSFGetID(?array $args):int
diff --git a/inc/registrar/config/seo/Resolver.php b/inc/registrar/config/seo/Resolver.php
index e754477..3f18043 100644
--- a/inc/registrar/config/seo/Resolver.php
+++ b/inc/registrar/config/seo/Resolver.php
@@ -5,15 +5,16 @@
 use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject;
 use JVBase\managers\SEO\render\Thing\Intangible\Quantity\Distance;
 use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 
 class Resolver {
-	protected static Meta $meta;
+	protected static ?Meta $meta;
 
-	public static function resolve(string $template, Meta $meta): string
+	public static function resolve(string $template, ?Meta $meta): string
 	{
 		self::$meta = $meta;
 		return preg_replace_callback(
@@ -22,17 +23,23 @@
 			$template
 		);
 	}
-	protected static function resolveVariable(string $variable, Meta $meta): string
+	protected static function resolveVariable(string $variable, ?Meta $meta = null): mixed
 	{
 		$variable = trim($variable);
-
+		switch ($variable) {
+			case 'CREATOR':
+				return JVB()->seo()->getCreator();
+				break;
+		}
 		if (str_contains($variable, '.')) {
 			return self::resolveRelation($variable, $meta);
 		}
-		if ($variable === 'post_permalink') {
+		if ($meta && $variable === 'post_permalink') {
 			return get_the_permalink($meta->id());
 		}
-
+		if (!$meta) {
+			return '';
+		}
 		$config = $meta->config($variable);
 		if (!$config) {
 			error_log('[SEO]Meta Resolver. Could not find meta configuration for variable: '.$variable);
@@ -45,7 +52,7 @@
 		};
 	}
 
-	protected static function resolveRelation(string $path, Meta $meta): string
+	protected static function resolveRelation(string $path, ?Meta $meta = null): string
 	{
 		$parts = explode('.', $path);
 		$relation = array_shift($parts);
@@ -53,6 +60,15 @@
 
 		//We need to:
 			// 1) Get the id of the item we're fetching (meta value of the $relation)
+		if ($relation === 'registrar') {
+			if (count($parts) === 2) {
+				return self::resolveRegistrar($parts[0], $parts[1]);
+			} else {
+				error_log('[Resolver]::resolveRelation: Registrar relation requires registrar.[slug].[property]');
+				return '';
+			}
+
+		}
 		$ID = $meta->get($relation);
 		if (!$ID || $ID === '') {
 			return '';
@@ -79,7 +95,7 @@
 		return self::resolve($field, $newMeta);
 	}
 
-	protected static function resolveImage(string $variable, Meta $meta, bool $returnID = false): string
+	protected static function resolveImage(string $variable, ?Meta $meta, bool $returnID = false): string
 	{
 		$imgID = $meta->get($variable);
 		if (!$imgID || $imgID === '') {
@@ -94,7 +110,7 @@
 		}
 		return $image[0];
 	}
-	protected static function resolveObject(string $variable, Meta $meta): string
+	protected static function resolveObject(string $variable, ?Meta $meta): string
 	{
 		//Hmmm... this should already be handled by dot notation.
 		return '';
@@ -105,7 +121,7 @@
 	 * We need to map the values to what schema.org expects.
 	 * Most are defined in the JVBase\managers\schema\render namespace.
 	 */
-	public static function resolveForSchema(string $property, string $value, mixed $schema, Meta $meta):mixed
+	public static function resolveForSchema(string $property, string $value, mixed $schema, ?Meta $meta = null):mixed
 	{
 		$check = 'resolve'.ucfirst($property).'Property';
 		if (method_exists(self::class, $check)) {
@@ -122,17 +138,20 @@
 		return self::resolve($value, $meta);
 	}
 
-		public static function checkPropertyType(string $property, mixed $value, mixed $schema, Meta $meta):mixed
+		public static function checkPropertyType(string $property, mixed $value, mixed $schema, ?Meta $meta):mixed
 		{
 			return match($property) {
 				'logo', 'image', 'photo', 'primaryImageOfPage', 'thumbnail', 'associatedMedia' => self::resolveImageProperty($property, $value, $schema, $meta),
+				'creator' => self::resolveCreator($property, $value, $schema, $meta),
 				default => false
 			};
-
 		}
 
-		public static function resolveImageProperty(string $property, mixed $value, mixed $schema, Meta $meta):?ImageObject
+		public static function resolveImageProperty(string $property, mixed $value, mixed $schema, ?Meta $meta):?ImageObject
 		{
+			if (!$meta) {
+				return null;
+			}
 			$value = str_replace('{{', '', str_replace('}}', '', $value));
 			$imgID = $meta->get($value);
 			error_log('Got image id: '.print_r($imgID, true));
@@ -143,7 +162,10 @@
 			if (!$img) {
 				return null;
 			}
-			Cache::for('imageSchemaObject')->flush();
+			if (JVB_TESTING){
+				Cache::for('imageSchemaObject')->flush();
+			}
+
 			return Cache::for('imageSchemaObject')->connect('post')->remember(
 				$imgID,
 				function () use ($imgID, $img) {
@@ -176,4 +198,29 @@
 			);
 
 		}
+
+		public static function resolveCreator(string $type, mixed $value, mixed $schema, ?Meta $meta):mixed
+		{
+			if (is_numeric($value)) {
+				//TODO generate from id
+
+			} else if (str_contains($value, 'CREATOR')) {
+				return JVB()->seo()->getCreator();
+			}
+			return '';
+		}
+
+		public static function resolveRegistrar(string $type, string $property):string
+		{
+			$registrar = Registrar::getInstance($type);
+			if (!$registrar) {
+				return '';
+			}
+			$method = 'get'.ucfirst($property);
+			if (!method_exists($registrar, $method)) {
+				error_log('[Resolver]::resolveRegistrar: Invalid property getter: '.$property);
+				return '';
+			}
+			return $registrar->$method();
+		}
 }
diff --git a/inc/registrar/config/seo/Schema.php b/inc/registrar/config/seo/Schema.php
index 91f03cb..b48b8de 100644
--- a/inc/registrar/config/seo/Schema.php
+++ b/inc/registrar/config/seo/Schema.php
@@ -34,7 +34,10 @@
 		'description' => '{{post_excerpt}}',
 	];
 
-	protected array $defaultArchive = [];
+	protected array $defaultArchive = [
+		'type'	=> 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage',
+		'title' => '{{registrar.plural}}'
+	];
 
 	public function __construct(string $slug, string $type)
 	{
@@ -52,6 +55,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\managers\SEO\render\Thing\CreativeWork\WebPage\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();
@@ -75,6 +88,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 +134,120 @@
 		}
 	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();
+			}
+
 			return $this->cache->remember(
 				$ID,
 				function () use ($ID) {
 					$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);
-					}
+					$class = $this->classFromConfig($config, $meta);
 
 					$class->setAuthor(JVB()->seo()->getCreator(true));
-					$schema = $class->outputSchema();
-					error_log('Generated archive schema: '.print_r($schema, true));
-					return $schema;
+					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 = get_option($action, apply_filters($action, $this->defaultSchema));
+					if (!array_key_exists('type', $config)) {
+						$config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
+					}
+					if (!class_exists($config['type'])) {
+						error_log('No class found for archive schema output: '.$config['type']);
+						return [];
+					}
+					$class = $this->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));
-
+					if (!array_key_exists('type', $config)) {
+						$config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
+					}
 					if (!class_exists($config['type'])) {
 						error_log('No class found for archive schema output: '.$config['type']);
 						return [];
 					}
+					$obj = get_queried_object();
+					$meta = (property_exists($obj, 'taxonomy')) ? Meta::forTerm($obj->term_id) : null;
 
-					$class = new $config['type'];
-					unset($config['type']);
-					foreach ($config as $property=>$value) {
-						$method = 'set'.ucfirst($property);
-						$class->$method($value);
-					}
+					$class = $this->classFromConfig($config, $meta);
+
 					$class->setIsPartOf(get_home_url().'/#website');
 					$itemList = new render\Thing\Intangible\ItemList\ItemList();
 					$items = new WP_Query([
@@ -191,7 +259,7 @@
 					$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->setPosition($pos);
 						$listItem->setItem($item);
@@ -209,24 +277,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);
-				$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);
+			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('archive');
+				$class = $this->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;
 			}
 		);
@@ -264,7 +351,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 = $this->getConfig()['type'];
 		if (!class_exists($class)) {
 			error_log('[SEO]Schema::defineReference Class not found: '.$class);
 			return;
@@ -294,4 +386,47 @@
 			$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'], $meta);
+		} elseif (is_post_type_archive($based) || is_tax($based)) {
+			$config = $this->getConfig('archive');
+			$title = $config['name'];
+		}
+		return $title;
+	}
+
+	protected function classFromConfig(array $config, ?Meta $meta = null):mixed
+	{
+		if (!array_key_exists('type', $config)) {
+			error_log('[Schema]::classFromConfig No class defined in config: '.print_r($config, true));
+			return false;
+		}
+		$className = $config['type'];
+		unset($config['type']);
+		$class = new $className();
+
+		foreach ($config as $property=>$value) {
+			if (is_array ($value)) {
+				$value = $this->classFromConfig($value, $meta);
+			}
+			$method = 'set'.ucfirst($property);
+			if (!method_exists($class, $method)) {
+				error_log('[Schema]::classFromConfig - method: '.$method.' does not exist in class: '.$className);
+				continue;
+			}
+			if (is_string($value) && str_contains($value, '{{')) {
+				$value = Resolver::resolveForSchema($property, $value, $config, $meta);
+			}
+			if (!empty($value)) {
+				$class->$method($value);
+			}
+		}
+		return $class;
+	}
 }
diff --git a/inc/registrar/fields/Field.php b/inc/registrar/fields/Field.php
index 6bbe401..e34c80e 100644
--- a/inc/registrar/fields/Field.php
+++ b/inc/registrar/fields/Field.php
@@ -37,12 +37,23 @@
 				return;
 			}
 		}
+		$current = get_class($this);
+		$class = match($config['type']) {
+			'group'	=> ($current !== GroupedField::class) ? new GroupedField($name, $config) : $this,
+			'select', 'radio', 'checkbox' => ($current !== OptionsField::class) ? new OptionsField($name, $config) : $this,
+			'repeater' => ($current !== RepeaterField::class) ? new RepeaterField($name, $config) : $this,
+			'taglist' => ($current !== TagListField::class) ? new TagListField($name, $config) : $this,
+			'taxonomy', 'post', 'user', 'selector' => ($current !== SelectorField::class) ? new SelectorField($name, $config) : $this,
+			'upload' => ($current !== UploadField::class) ? new UploadField($name, $config) : $this,
+			default => $this
+		};
+
 		foreach ($config as $key => $value) {
-			if (property_exists($this, $key)) {
+			if (property_exists($class, $key)) {
 				$method = 'set' . ucfirst($key);
-				$this->$method($value);
+				$class->$method($value);
 			} else {
-				error_log('Instance: '.print_r($this, true));
+				error_log('Instance: '.print_r($class, true));
 				error_log('[JVBase\registrar\Field] Invalid key for '.$name.': '.$key);
 			}
 		}
diff --git a/inc/registrar/fields/GroupedField.php b/inc/registrar/fields/GroupedField.php
index 99a8211..b82c322 100644
--- a/inc/registrar/fields/GroupedField.php
+++ b/inc/registrar/fields/GroupedField.php
@@ -13,12 +13,18 @@
 	{
 		foreach ($fields as $name => $config) {
 			$this->fields[$name] = match ($config['type']) {
-				'upload', 'image', 'gallery' 	=> new Upload($name, $config),
+				'upload', 'image', 'gallery' 	=> new UploadField($name, $config),
 				'checkbox', 'radio', 'select', 'set' => new OptionsField($name, $config),
-				'repeater', 'group', 'tagList' => new GroupedField($name, $config),
-				'selector', 'taxonomy', 'user', 'post' => new TaxonomyField($name, $config),
+				'repeater' => new RepeaterField($name, $config),
+				'tagList'	=> new TagListField($name, $config),
+				'group'	=> new GroupedField($name, $config),
+				'selector', 'taxonomy', 'user', 'post' => new SelectorField($name, $config),
 				default => new Field($name, $config),
 			};
 		}
 	}
+	public function getFields():array
+	{
+		return $this->fields;
+	}
 }
diff --git a/inc/registrar/fields/RepeaterField.php b/inc/registrar/fields/RepeaterField.php
new file mode 100644
index 0000000..601634a
--- /dev/null
+++ b/inc/registrar/fields/RepeaterField.php
@@ -0,0 +1,50 @@
+<?php
+namespace JVBase\registrar\fields;
+
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class RepeaterField extends GroupedField {
+	protected array $fields = [];
+	protected string $add_label = 'Add Row';
+	protected string $row_label = 'New Item';
+
+	public function setAdd_label(string $label):void
+	{
+		$this->add_label = $label;
+	}
+	public function getAdd_label():string
+	{
+		return $this->add_label;
+	}
+	protected function checkFieldVariables(string $variables):bool
+	{
+		if (!str_contains($variables, '{{')) {
+			return false;
+		}
+		$fields = array_filter(array_map(function($field) { return str_replace('}}','',$field);}, explode('{{',$variables)));
+		foreach ($fields as $field) {
+			if (!array_key_exists($field, $this->fields)) {
+				error_log('['.(new \ReflectionClass($this))->getShortName().']Invalid field attempted: '.$field);
+				return false;
+			}
+		}
+		return true;
+	}
+	public function setRow_label(string $label):void
+	{
+		if (str_contains($label, '{{')) {
+			if (!$this->checkFieldVariables($label)) {
+				return;
+			}
+		}
+
+		$this->row_label = $label;
+	}
+	public function getRow_label():string
+	{
+		return $this->row_label;
+	}
+}
diff --git a/inc/registrar/fields/SelectorField.php b/inc/registrar/fields/SelectorField.php
new file mode 100644
index 0000000..d41145c
--- /dev/null
+++ b/inc/registrar/fields/SelectorField.php
@@ -0,0 +1,125 @@
+<?php
+namespace JVBase\registrar\fields;
+
+use JVBase\registrar\Registrar;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class SelectorField extends Field {
+	protected string $taxonomy;
+	protected string $post_type;
+	protected string $user_role;
+	protected int $max = 0;
+	protected bool $search = true;
+	protected bool $createNew = false;
+	protected bool $autocomplete = true;
+	/**
+	 * @var bool isReference If true, does not set default taxonomy functionality, but is a reference to the selected items. (example: if it's a taxonomy subtype, does not set_object_terms with the selection)
+	 */
+	protected bool $isReference = false;
+	protected bool $update = true;
+	protected string $subtype;
+	protected array $allowedSubtype = ['taxonomy', 'user', 'post'];
+
+	public function setTaxonomy(string $taxonomy):void
+	{
+		$allowed = array_merge(['tag', 'category'], Registrar::getRegistered('term'));
+		if (!in_array(jvbNoBase($taxonomy), $allowed)) {
+			error_log('[SelectorField]Attempted taxonomy not allowed: '.$taxonomy);
+			return;
+		}
+		$this->taxonomy = $taxonomy;
+	}
+	public function getTaxonomy():string {
+		return $this->taxonomy;
+	}
+
+	public function setPost_type(string $post_type):void
+	{
+		$allowed = array_merge(['post', 'page'], Registrar::getRegistered('post'));
+		if (!in_array(jvbNoBase($post_type), $allowed)) {
+			error_log('[SelectorField]Attempted post type not allowed: '.$post_type);
+			return;
+		}
+		$this->post_type = $post_type;
+	}
+	public function getPost_type():string {
+		return $this->post_type;
+	}
+
+	public function setUser_role(string $user_role):void
+	{
+		$allowed = array_merge(['administrator'], Registrar::getRegistered('user'));
+		if (!in_array(jvbNoBase($user_role), $allowed)) {
+			error_log('[SelectorField]Attempted user not allowed: '.$user_role);
+			return;
+		}
+		$this->user_role = $user_role;
+	}
+	public function getUser_role():string {
+		return $this->user_role;
+	}
+
+	public function setMax(int $max):void
+	{
+		$this->max = $max;
+	}
+	public function getMax():int
+	{
+		return $this->max;
+	}
+	public function setSearch(bool $search):void {
+		$this->search = $search;
+	}
+	public function getSearch():bool
+	{
+		return $this->search;
+	}
+	public function setCreateNew(bool $createNew):void
+	{
+		$this->createNew = $createNew;
+	}
+	public function getCreateNew():bool
+	{
+		return $this->createNew;
+	}
+	public function setAutocomplete(bool $autocomplete):void
+	{
+		$this->autocomplete = $autocomplete;
+	}
+	public function getAutocomplete():bool
+	{
+		return $this->autocomplete;
+	}
+	public function setUpdate(bool $update):void
+	{
+		$this->update = $update;
+	}
+	public function getUpdate():bool
+	{
+		return $this->update;
+	}
+	public function setSubtype(string $subtype):void
+	{
+		if (!in_array($subtype, $this->allowedSubtype)) {
+			error_log('[SelectorField]Attempted subtype not allowed: '.$subtype);
+			return;
+		}
+		$this->subtype = $subtype;
+	}
+	public function getSubtype():string
+	{
+		return $this->subtype;
+	}
+
+	public function setIsReference(bool $isReference):void
+	{
+		$this->isReference = $isReference;
+	}
+	public function getIsReference():bool
+	{
+		return $this->isReference;
+	}
+}
diff --git a/inc/registrar/fields/TagListField.php b/inc/registrar/fields/TagListField.php
new file mode 100644
index 0000000..5aef076
--- /dev/null
+++ b/inc/registrar/fields/TagListField.php
@@ -0,0 +1,33 @@
+<?php
+namespace JVBase\registrar\fields;
+
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class TagListField extends RepeaterField {
+	protected string $tag_format = 'first_field';
+	protected string $add_label = 'Add';
+
+	public function setTag_format(string $tag_format):void
+	{
+		if (str_contains($tag_format, '{{')) {
+			if (!$this->checkFieldVariables($tag_format)) {
+				return;
+			}
+			$this->tag_format = $tag_format;
+			return;
+		}
+
+		if (!in_array($tag_format, ['first_field', 'all_fields'])) {
+			error_log('[TagListField]Could not validate tag format: '.$tag_format);
+			return;
+		}
+		$this->tag_format = $tag_format;
+	}
+	public function getTag_format():string
+	{
+		return $this->tag_format;
+	}
+}
diff --git a/inc/registrar/fields/TaxonomyField.php b/inc/registrar/fields/TaxonomyField.php
deleted file mode 100644
index da92231..0000000
--- a/inc/registrar/fields/TaxonomyField.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-namespace JVBase\registrar\fields;
-
-if (!defined('ABSPATH')) {
-	exit;
-}
-
-class TaxonomyField extends Field {
-	protected bool $autocomplete;
-	protected string $taxonomy;
-	protected string $taxonomy_type;
-
-	public function setTaxonomy(string $taxonomy) {
-		$this->taxonomy = $taxonomy;
-	}
-	public function setTaxonomy_type(string $taxonomy_type) {
-		$this->taxonomy_type = $taxonomy_type;
-	}
-}
diff --git a/inc/registrar/fields/Upload.php b/inc/registrar/fields/UploadField.php
similarity index 95%
rename from inc/registrar/fields/Upload.php
rename to inc/registrar/fields/UploadField.php
index 8f04148..dfc2f1b 100644
--- a/inc/registrar/fields/Upload.php
+++ b/inc/registrar/fields/UploadField.php
@@ -6,7 +6,7 @@
 	exit;
 }
 
-class Upload extends Field {
+class UploadField extends Field {
 	protected bool $multiple = false;
 	protected string $subtype;
 
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 7f12b55..ea70a8b 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -280,8 +280,9 @@
 		// Run query
 		$query = new WP_Query($args);
 
+		$registrar = Registrar::getInstance(str_replace('-', '_', $this->post_type));
+		$this->fields = $registrar->getFields()??[];
 
-		$this->fields = Registrar::getFieldsFor(str_replace('-', '_', $this->post_type));
 		$this->taxonomies = $this->getTaxonomies($this->post_type);
 		$posts = array_map([$this, 'prepareItem'], $query->posts);
 
@@ -480,7 +481,7 @@
 		$get = [];
 		$fields = (empty($fields)) ? $this->fields : $fields;
 		$get = $this->getUploadFields($fields);
-
+		error_log('Upload fields: '.print_r($get, true));
 
 		if (!empty($get)) {
 			$actualGet = array_map(function($fieldName) {
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index c8eeda1..5bb76e4 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -216,14 +216,18 @@
 	 */
 	public function addTaxonomyFilter(array $taxonomies, ?string $limit = null): self {
 		foreach($taxonomies as $taxonomy) {
+			error_log('Fetchinig taxonomy: '.print_r($taxonomy, true));
 			$registrar = Registrar::getInstance($taxonomy);
-			$this->taxonomies[$taxonomy] = [
-				'type'	=> 'taxonomy',
-				'taxonomy'=> $taxonomy,
-				'limit'	=> $limit,
-				'label'	=> $registrar->getPlural(),
-				'icon'	=> $registrar->getIcon()
-			];
+
+			if ($registrar) {
+				$this->taxonomies[$taxonomy] = [
+					'type'	=> 'taxonomy',
+					'taxonomy'=> $taxonomy,
+					'limit'	=> $limit,
+					'label'	=> $registrar->getPlural(),
+					'icon'	=> $registrar->getIcon()
+				];
+			}
 		}
 
 		return $this;
@@ -977,7 +981,8 @@
 					<option value="<?=$control?>"<?=$disabled?>><?=$label?></option>
 					<?php
 				}
-				foreach ($this->taxonomies as $taxonomy) {
+
+				foreach ($this->taxonomies as $taxonomy =>$config) {
 					$registrar = Registrar::getInstance($taxonomy);
 					if (!$registrar) continue;
 					?>
@@ -1624,7 +1629,6 @@
 						$section = (array_key_exists('section', $config)) ? $config['section'] : 'basic';
 						$tabs[$section]['content'] .= Form::render($n, '', $config);
 					} else {
-						jvbDump($config, $n);
 						echo Form::render($n, '', $config);
 					}
 				}
diff --git a/inc/users/UserSettings.php b/inc/users/UserSettings.php
index 1ee08e9..87a3659 100644
--- a/inc/users/UserSettings.php
+++ b/inc/users/UserSettings.php
@@ -126,17 +126,19 @@
                 'section'   => 'newsletter',
             ],
             'owner_of'    => [
-                'type' => 'taxonomy',
+                'type' => 'selector',
+				'subtype'=> 'taxonomy',
                 'label'    => __('Owner of', 'jvb'),
-                'taxonomy_type' => 'reference',
+                'isReference' => true,
                 'taxonomy' => BASE. 'shop',
                 'quickEdit'    => true,
                 'default'    => '',
             ],
             'manager_of'    => [
-                'type' => 'taxonomy',
+                'type' => 'selector',
+                'subtype' => 'taxonomy',
                 'label'    => __('Manager of', 'jvb'),
-                'taxonomy_type' => 'reference',
+                'isReference' => true,
                 'taxonomy' => BASE. 'shop',
                 'hidden'    => true,
                 'default'    => '',
diff --git a/jvb.php b/jvb.php
index 21bc092..6bbe5ef 100644
--- a/jvb.php
+++ b/jvb.php
@@ -82,9 +82,13 @@
 {
 	return [BASE.'directory', BASE.'dash', 'attachment', 'revision', 'nav_menu_item'];
 }
-add_filter('show_admin_bar', '__return_false');
+
 
 define('JVB_TESTING', str_contains(get_home_url(),'.test'));
+
+if (JVB_TESTING) {
+	add_filter('show_admin_bar', '__return_false');
+}
 //if (JVB_TESTING) {
 //	error_log('In testing mode...');
 //} else {

--
Gitblit v1.10.0