graphql_single_name) { $carry[wp_gql_seo_get_field_key($post_type_object->graphql_single_name)] = ['type' => 'SEOContentType']; } } return $carry; } /** * Build taxonomy types data array for GraphQL schema. * * @param array $taxonomies Taxonomies. * @return array */ function wp_gql_seo_build_taxonomy_types($taxonomies) { $carry = []; foreach ($taxonomies as $taxonomy) { $taxonomy_object = get_taxonomy($taxonomy); if ($taxonomy_object->graphql_single_name) { $carry[wp_gql_seo_get_field_key($taxonomy_object->graphql_single_name)] = ['type' => 'SEOTaxonomyType']; } } return $carry; } /** * Get full head content from Yoast SEO. * * Delegates to wp_gql_seo_get_head_obj to avoid calling get_head() twice * when both fullHead and head are requested for the same meta object. * * @param \Yoast\WP\SEO\Surfaces\Values\Meta|bool $metaForPost Meta object. * @return string */ function wp_gql_seo_get_full_head($metaForPost) { return wp_gql_seo_get_head_obj($metaForPost)['html']; } /** * Get structured head (html + json) from Yoast SEO. * * @param \Yoast\WP\SEO\Surfaces\Values\Meta|bool $metaForPost Meta object. * @return array */ function wp_gql_seo_get_head_obj($metaForPost) { static $memo = []; if ($metaForPost !== false) { $key = spl_object_hash($metaForPost); if (!isset($memo[$key])) { $head = $metaForPost->get_head(); $html = is_string($head) ? $head : $head->html; $json = is_object($head) && isset($head->json) ? $head->json : []; // Keep the meta object referenced so its hash cannot be reused. $memo[$key] = [ 'meta' => $metaForPost, 'data' => [ 'html' => $html, 'json' => wp_json_encode($json, JSON_UNESCAPED_SLASHES), ], ]; } return $memo[$key]['data']; } return [ 'html' => '', 'json' => wp_json_encode([], JSON_UNESCAPED_SLASHES), ]; } /** * Safely read a property from a Yoast Indexable (or any object/array). * * @param mixed $indexable The indexable object (or null/false). * @param string $key The property key. * @return mixed|null */ function wp_gql_seo_indexable_prop($indexable, $key) { if (!is_object($indexable) || !isset($indexable->{$key})) { return null; } return $indexable->{$key}; } /** * Build the SEOAnalysis data array from an Indexable. * * @param mixed $indexable The indexable object (or null/false). * @return array */ function wp_gql_seo_build_analysis($indexable) { if (!is_object($indexable)) { return [ 'keywordScore' => null, 'readabilityScore' => null, 'inclusiveLanguageScore' => null, 'linkCount' => null, 'incomingLinkCount' => null, ]; } return [ 'keywordScore' => isset($indexable->primary_focus_keyword_score) ? (int) $indexable->primary_focus_keyword_score : null, 'readabilityScore' => isset($indexable->readability_score) ? (int) $indexable->readability_score : null, 'inclusiveLanguageScore' => isset($indexable->inclusive_language_score) ? (int) $indexable->inclusive_language_score : null, 'linkCount' => isset($indexable->link_count) ? (int) $indexable->link_count : null, 'incomingLinkCount' => isset($indexable->incoming_link_count) ? (int) $indexable->incoming_link_count : null, ]; } /** * Build the SEORobots data array from Yoast robots array and Indexable. * * @param array $robots The robots array from Meta (index/follow keys). * @param mixed $indexable The indexable object (or null/false). * @return array */ function wp_gql_seo_build_robots($robots, $indexable) { return [ 'noindex' => isset($robots['index']) ? wp_gql_seo_format_string($robots['index']) : null, 'nofollow' => isset($robots['follow']) ? wp_gql_seo_format_string($robots['follow']) : null, 'noarchive' => boolval(wp_gql_seo_indexable_prop($indexable, 'is_robots_noarchive')), 'noimageindex' => boolval(wp_gql_seo_indexable_prop($indexable, 'is_robots_noimageindex')), 'nosnippet' => boolval(wp_gql_seo_indexable_prop($indexable, 'is_robots_nosnippet')), ]; } /** * Build the Premium social archive data from Yoast SEO Premium options. * * Returns null for title/description and null for image when the Premium * option keys are absent (graceful free-tier fallback). * * @param string $option_prefix The option prefix (e.g. 'ptarchive-post', 'tax-category', 'author-wpseo', 'archive-wpseo'). * @param array $all All Yoast SEO options. * @param AppContext $context The GraphQL AppContext for deferred image loading. * @return array */ function wp_gql_seo_get_premium_social($option_prefix, $all, $context) { $title_key = 'social-title-' . $option_prefix; $desc_key = 'social-description-' . $option_prefix; $image_id_key = 'social-image-id-' . $option_prefix; $has_premium = isset($all[$title_key]) || isset($all[$desc_key]) || isset($all[$image_id_key]); if (!$has_premium) { return [ 'title' => null, 'description' => null, 'image' => null, ]; } return [ 'title' => wp_gql_seo_format_string(wp_gql_seo_replace_vars($all[$title_key] ?? null)), 'description' => wp_gql_seo_format_string(wp_gql_seo_replace_vars($all[$desc_key] ?? null)), 'image' => $context->get_loader('post')->load_deferred(absint($all[$image_id_key] ?? 0)), ]; } /** * Build content type data for GraphQL schema. * * @param array $types Post types. * @param array $all All Yoast SEO options. * @param AppContext $context The GraphQL AppContext for deferred image loading. * @return array */ function wp_gql_seo_build_content_type_data($types, $all, $context) { $carry = []; // Validate input parameters if (!is_array($types) || empty($types) || !is_array($all) || empty($all)) { return $carry; } foreach ($types as $type) { $post_type_object = get_post_type_object($type); // Validate post type object if (!$post_type_object || !$post_type_object->graphql_single_name) { continue; } $tag = wp_gql_seo_get_field_key($post_type_object->graphql_single_name); $meta_cache = false; $get_meta = function () use ($type, &$meta_cache) { if (false === $meta_cache) { $meta_cache = ['v' => YoastSEO()->meta->for_post_type_archive($type)]; } return $meta_cache['v']; }; $carry[$tag] = [ 'title' => wp_gql_seo_format_string(wp_gql_seo_replace_vars($all['title-' . $type] ?? null)), 'metaDesc' => wp_gql_seo_format_string(wp_gql_seo_replace_vars($all['metadesc-' . $type] ?? null)), 'metaRobotsNoindex' => boolval($all['noindex-' . $type] ?? false), 'schemaType' => $all['schema-page-type-' . $type] ?? null, 'articleType' => $all['schema-article-type-' . $type] ?? null, 'schema' => function () use ($get_meta) { return [ 'raw' => function () use ($get_meta) { $meta = $get_meta(); return !empty($meta) && !empty($meta->schema) ? wp_json_encode($meta->schema, JSON_UNESCAPED_SLASHES) : null; }, ]; }, 'archive' => function () use ($get_meta, $post_type_object, $type, $all, $context) { $meta = $get_meta(); return [ 'hasArchive' => boolval($post_type_object->has_archive), 'archiveLink' => apply_filters('wp_gql_seo_archive_link', get_post_type_archive_link($type), $type), 'title' => wp_gql_seo_format_string($meta !== false ? $meta->title : null), 'metaDesc' => wp_gql_seo_format_string($all['metadesc-ptarchive-' . $type] ?? null), 'metaRobotsNoindex' => $meta !== false && !empty($meta->robots['index']) && $meta->robots['index'] === 'index' ? false : true, 'metaRobotsNofollow' => $meta !== false && !empty($meta->robots['follow']) && $meta->robots['follow'] === 'follow' ? false : true, 'metaRobotsIndex' => $meta !== false ? ($meta->robots['index'] ?? 'noindex') : 'noindex', 'metaRobotsFollow' => $meta !== false ? ($meta->robots['follow'] ?? 'nofollow') : 'nofollow', 'breadcrumbTitle' => wp_gql_seo_format_string($all['bctitle-ptarchive-' . $type] ?? null), 'fullHead' => function () use ($meta) { return wp_gql_seo_get_full_head($meta); }, 'head' => function () use ($meta) { return wp_gql_seo_get_head_obj($meta); }, 'social' => wp_gql_seo_get_premium_social('ptarchive-' . $type, $all, $context), ]; }, ]; } return $carry; } /** * Build taxonomy data for GraphQL schema. * * @param array $taxonomies Taxonomies. * @param array $all All Yoast SEO options. * @param AppContext $context The GraphQL AppContext for deferred image loading. * @return array */ function wp_gql_seo_build_taxonomy_data($taxonomies, $all, $context) { $carry = []; // Validate input parameters if (!is_array($taxonomies) || empty($taxonomies) || !is_array($all) || empty($all)) { return $carry; } foreach ($taxonomies as $taxonomy) { $taxonomy_object = get_taxonomy($taxonomy); // Validate taxonomy object if (!$taxonomy_object || !$taxonomy_object->graphql_single_name) { continue; } $tag = wp_gql_seo_get_field_key($taxonomy_object->graphql_single_name); $carry[$tag] = [ 'archive' => [ 'title' => wp_gql_seo_format_string(wp_gql_seo_replace_vars($all['title-tax-' . $taxonomy] ?? null)), 'metaDesc' => wp_gql_seo_format_string( wp_gql_seo_replace_vars($all['metadesc-tax-' . $taxonomy] ?? null) ), 'metaRobotsNoindex' => boolval($all['noindex-tax-' . $taxonomy] ?? false), 'social' => wp_gql_seo_get_premium_social('tax-' . $taxonomy, $all, $context), ], ]; } return $carry; }