| | |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\managers\SEO\schemas\SchemaDefinition; |
| | | use WP_Term; |
| | | use WP_User; |
| | | |
| | |
| | | $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, |
| | |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * Enhanced buildSchemaFromConfig with Meta 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 Meta if we have a context |
| | | $meta = null; |
| | | $context = $this->getCurrentContext(); |
| | | if ($context) { |
| | | $meta = new Meta($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); |
| | | } |
| | | |
| | | /** |