From c348d35c7ecb6c74f71cf90b982412f267c5d807 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 10 Feb 2026 02:19:05 +0000
Subject: [PATCH] =minor fixes to schema system
---
inc/managers/SEO/SchemaOutputManager.php | 286 +++++++++++++++++++++++----------------------------------
1 files changed, 115 insertions(+), 171 deletions(-)
diff --git a/inc/managers/SEO/SchemaOutputManager.php b/inc/managers/SEO/SchemaOutputManager.php
index 051eeb7..9c4a545 100644
--- a/inc/managers/SEO/SchemaOutputManager.php
+++ b/inc/managers/SEO/SchemaOutputManager.php
@@ -1,8 +1,10 @@
<?php
namespace JVBase\managers\SEO;
-use JVBase\managers\CacheManager;
-use JVBase\meta\MetaManager;
+use JVBase\managers\Cache;
+use JVBase\managers\SEO\schemas\SchemaResolverRegistry;
+use JVBase\meta\Meta;
+use JVBase\managers\SEO\schemas\SchemaDefinition;
use WP_Term;
use WP_User;
@@ -16,14 +18,14 @@
* Integrates with The SEO Framework, letting it handle defaults
* while we override with our configured templates.
*
- * Now with integrated caching via CacheManager for performance.
+ * Now with integrated caching via Cache for performance.
*/
class SchemaOutputManager
{
private ConfigManager $config;
private SchemaBuilder $registry;
private ?TemplateResolver $resolver = null;
- private CacheManager $cache;
+ private Cache $cache;
private array $pseudoTypes = [
'BeforeAfter',
];
@@ -31,12 +33,10 @@
public function __construct()
{
$this->registry = SchemaBuilder::getInstance();
- $this->cache = CacheManager::for('schema');
-
- // Register cache connections
- $this->cache->connectTo('post', 'id');
- $this->cache->connectTo('taxonomy', 'id');
- $this->cache->connectTo('user', 'id');
+ $this->cache = Cache::for('schema')
+ ->connect('post',true)
+ ->connect('taxonomy',true)
+ ->connect('user',true);
// Hook into TSF for meta
add_filter('the_seo_framework_title_from_generation', [$this, 'filterTitle'], 10, 2);
@@ -50,6 +50,69 @@
// Output our schema
add_action('wp_head', [$this, 'outputSchema'], 1);
+ add_filter('the_seo_framework_sitemap_exclude_ids', [$this, 'excludeHiddenSingles'], 10, 1);
+ }
+
+ /**
+ * Exclude posts from sitemap based on hide_single and is_timeline flags
+ *
+ * @param array $ids Array of post IDs to exclude
+ * @return array Modified array with hidden posts added
+ */
+ public function excludeHiddenSingles(array $ids): array
+ {
+ $hiddenTypes = [];
+ $timelineTypes = [];
+
+ // Find post types with hide_single or is_timeline flags
+ foreach (JVB_CONTENT as $slug => $config) {
+ $postType = BASE . $slug;
+
+ if (!empty($config['hide_single'])) {
+ $hiddenTypes[] = $postType;
+ }
+
+ if (!empty($config['is_timeline'])) {
+ $timelineTypes[] = $postType;
+ }
+ }
+
+ $hiddenIds = [];
+
+ // Get all posts from hide_single types
+ if (!empty($hiddenTypes)) {
+ $hiddenIds = $this->cache->remember(
+ 'hidden_single_posts',
+ function() use ($hiddenTypes) {
+ return get_posts([
+ 'post_type' => $hiddenTypes,
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ 'post_status' => 'publish',
+ ]);
+ }
+ );
+ }
+
+ // Get child posts from timeline types
+ if (!empty($timelineTypes)) {
+ $timelineChildIds = $this->cache->remember(
+ 'timeline_child_posts',
+ function() use ($timelineTypes) {
+ return get_posts([
+ 'post_type' => $timelineTypes,
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ 'post_status' => 'publish',
+ 'post_parent__not_in' => [0], // Only get posts with a parent
+ ]);
+ }
+ );
+
+ $hiddenIds = array_merge($hiddenIds, $timelineChildIds);
+ }
+
+ return array_merge($ids, $hiddenIds);
}
/**
@@ -58,7 +121,6 @@
public function filterTitle(string $title, ?array $args): string
{
if ($args !== null) {
- // Not in the loop (admin, etc.)
return $title;
}
@@ -69,14 +131,22 @@
$metaConfig = $this->config->meta();
- if (empty($metaConfig['title'])) {
+ if (empty($metaConfig['metaTitle'])) {
return $title;
}
$resolver = $this->getResolver();
- $customTitle = $resolver->resolve($metaConfig['title']);
+ $customTitle = $resolver->resolve($metaConfig['metaTitle']);
- return $customTitle ?: $title;
+ if (!$customTitle) {
+ return $title;
+ }
+
+ // Strip trailing site name — TSF adds its own branding
+ $siteName = get_bloginfo('name');
+ $customTitle = preg_replace('/\s*[|\-–—]\s*' . preg_quote($siteName, '/') . '\s*$/i', '', $customTitle);
+
+ return $customTitle;
}
/**
@@ -95,12 +165,12 @@
$metaConfig = $this->config->meta();
- if (empty($metaConfig['description'])) {
+ if (empty($metaConfig['metaDescription'])) {
return $description;
}
$resolver = $this->getResolver();
- $customDescription = $resolver->resolve($metaConfig['description']);
+ $customDescription = $resolver->resolve($metaConfig['metaDescription']);
// Truncate to reasonable length
if (strlen($customDescription) > 160) {
@@ -127,16 +197,16 @@
$metaConfig = $this->config->meta();
// Check for custom image
- if (!empty($metaConfig['image'])) {
+ if (!empty($metaConfig['socialPreviewImage'])) {
$resolver = $this->getResolver();
- $imageUrl = $resolver->resolve($metaConfig['image']);
+ $imageUrl = $resolver->resolve($metaConfig['socialPreviewImage']);
if ($imageUrl) {
$params['og:image'] = $imageUrl;
// Use twitter-specific image if set, otherwise use main image
- if (!empty($metaConfig['twitter_image'])) {
- $twitterImage = $resolver->resolve($metaConfig['twitter_image']);
+ if (!empty($metaConfig['twitterImage'])) {
+ $twitterImage = $resolver->resolve($metaConfig['twitterImage']);
$params['twitter:image'] = $twitterImage ?: $imageUrl;
} else {
$params['twitter:image'] = $imageUrl;
@@ -323,10 +393,10 @@
$resolver = $this->getResolver();
$schemaType = $schemaConfig['type'];
- // Resolve all field values from templates
+ // Resolve templates (resolver handles transformation)
$resolvedConfig = $this->resolveConfigTemplates($schemaConfig, $resolver);
- // Build schema with resolved values
+ // Build via resolver system
$schema = $this->buildSchemaFromConfig(
$resolvedConfig,
$schemaType,
@@ -344,96 +414,30 @@
return $schema;
}
/**
- * Build schema for archive pages
- * Automatically generates mainEntity from archive posts
+ * Build schema for archive pages.
+ * mainEntity is now handled by CollectionPageResolver::getAutoFields()
*/
private function buildArchiveSchema(array $context): ?array
{
- // Ensure archive config is initialized
if (!$this->config->archive()) {
$this->config->setupArchive();
}
$archiveConfig = $this->config->archive();
- // Return null if no config or no type defined
if (empty($archiveConfig) || empty($archiveConfig['type'])) {
return null;
}
$resolver = $this->getResolver();
- $schemaType = $archiveConfig['type'];
-
- // Resolve templates from archive config
$resolvedConfig = $this->resolveConfigTemplates($archiveConfig, $resolver);
- // Build base schema
- $schema = $this->buildSchemaFromConfig(
+ // Resolver handles mainEntity auto-enrichment now
+ return $this->buildSchemaFromConfig(
$resolvedConfig,
- $schemaType,
- $resolver->resolveVariable('permalink') . '#' . strtolower($schemaType)
+ $archiveConfig['type'],
+ $resolver->resolveVariable('permalink') . '#' . strtolower($archiveConfig['type'])
);
-
- if (!$schema) {
- return null;
- }
-
- // Automatically add mainEntity for types that need it
- $mainEntity = $this->buildMainEntity($schemaType, $context['type']);
- if ($mainEntity) {
- $schema['mainEntity'] = $mainEntity;
- }
-
- return $schema;
- }
-
- /**
- * Automatically build mainEntity for archive pages
- * Uses SchemaReferenceBuilder to generate entities from archive posts
- *
- * @param string $archiveSchemaType The archive's @type (FAQPage, CollectionPage, etc.)
- * @param string $contentType The content type being archived (faq, artwork, etc.)
- * @return array|null Array of entities or null if not applicable
- */
- private function buildMainEntity(string $archiveSchemaType, string $contentType): ?array
- {
- // Only certain archive types need mainEntity
- $typesNeedingMainEntity = ['FAQPage', 'CollectionPage', 'ItemList'];
- if (!in_array($archiveSchemaType, $typesNeedingMainEntity)) {
- return null;
- }
-
- $context = $this->getCurrentContext();
-
- // For taxonomy term archives, get posts from the term
- if ($context['objectType'] === 'term') {
- // Get the post type(s) this taxonomy is for
- $taxonomy = defined('JVB_TAXONOMY') && isset(JVB_TAXONOMY[$contentType])
- ? JVB_TAXONOMY[$contentType]
- : null;
-
- if (!$taxonomy || empty($taxonomy['for_content'])) {
- return null;
- }
-
- // Use the first post type (most common case)
- $postType = $taxonomy['for_content'][0];
-
- return SchemaReferenceBuilder::buildFromTerm(
- $context['objectId'],
- $postType,
- 10, // limit
- null, // auto-infer type
- true // include context
- );
- }
-
- // For post type archives
- if ($context['objectType'] === 'archive') {
- return SchemaReferenceBuilder::buildFromArchive($contentType);
- }
-
- return null;
}
/**
@@ -459,86 +463,26 @@
}
/**
- * Enhanced buildSchemaFromConfig with MetaManager integration
+ * Build schema from config using the resolver system.
+ *
+ * Replaces the old double-transform approach with a single-pass
+ * resolver that handles template resolution and transformation.
*/
private function buildSchemaFromConfig(array $config, string $schemaType, ?string $id = null): ?array
{
- // Build base schema
- $schema = ['@type' => $this->resolveSchemaType($schemaType)];
-
- if ($id) {
- $schema['@id'] = $id;
- }
-
- // Get MetaManager if we have a context
- $meta = null;
$context = $this->getCurrentContext();
- if ($context) {
- $meta = new MetaManager($context['objectId'], $context['objectType']);
- }
- // Process each field
- foreach ($config as $fieldName => $value) {
- // Skip meta fields and empty values
- if ($fieldName === 'type' || $value === null || $value === '' || $value === []) {
- continue;
- }
+ $definition = SchemaDefinition::fromContext(
+ $this->resolveSchemaType($schemaType),
+ $config,
+ $id,
+ $context
+ );
- // Auto-resolve field value (handles images, locations, etc.)
- $value = SchemaFieldHelpers::autoResolve($fieldName, $value, $meta);
+ $resolver = SchemaResolverRegistry::getInstance()->get($schemaType);
+ $meta = $context ? new Meta($context['objectId'], $context['objectType']) : null;
- // Get field definition for transformer
- $fieldDef = $this->registry->getFieldDefinition($fieldName);
-
- // Apply transformer if defined
- if ($fieldDef && !empty($fieldDef['transformer'])) {
- $value = $this->applyTransformer($value, $fieldDef['transformer'], $fieldName);
- }
-
- // Skip if empty after transformation
- if ($value === null || $value === '' || $value === []) {
- continue;
- }
-
- // Handle multi-property transformers (like location_complex returns address + geo)
- if (is_array($value) && !isset($value['@type']) && !isset($value[0])) {
- $multiProps = ['address', 'geo', 'openingHours', 'sameAs'];
- if (!empty(array_intersect(array_keys($value), $multiProps))) {
- foreach ($value as $subKey => $subValue) {
- if ($subValue !== null && $subValue !== '' && $subValue !== []) {
- $schema[$subKey] = $subValue;
- }
- }
- continue;
- }
- }
-
- // Normal case: add single property
- $schema[$fieldName] = $value;
- }
-
- // Return null if only @type remains
- return (count($schema) > 1) ? $schema : null;
- }
-
- /**
- * Apply transformer to a field value
- */
- private function applyTransformer(mixed $value, string $transformer, string $fieldName): mixed
- {
- // Check if transformer method exists in SchemaFieldHelpers
- if (method_exists(SchemaFieldHelpers::class, $transformer)) {
- try {
- return SchemaFieldHelpers::$transformer($value);
- } catch (\Throwable $e) {
- // Log error but don't break schema output
- error_log("Schema transformer error for {$fieldName}: {$e->getMessage()}");
- return $value;
- }
- }
-
- // No transformer found, return value as-is
- return $value;
+ return $resolver->resolve($definition, $meta);
}
/**
@@ -596,12 +540,12 @@
$resolver = $this->getResolver();
$metaConfig = $this->config->meta();
- if (!empty($metaConfig['title'])) {
- $webpage['name'] = $resolver->resolve($metaConfig['title']);
+ if (!empty($metaConfig['metaTitle'])) {
+ $webpage['name'] = $resolver->resolve($metaConfig['metaTitle']);
}
- if (!empty($metaConfig['description'])) {
- $webpage['description'] = $resolver->resolve($metaConfig['description']);
+ if (!empty($metaConfig['metaDescription'])) {
+ $webpage['description'] = $resolver->resolve($metaConfig['metaDescription']);
}
}
--
Gitblit v1.10.0