/** * Replace your existing nadcab_get_hire_pages_with_seo() body with this implementation. * Keeps caching, ACF, featured image, etc. — but reliably extracts Rank Math meta & JSON-LD. */ function nadcab_get_hire_pages_with_seo() { // ✅ Unique Hire Page IDs (same as you had) $include_ids = array_unique([ 29072, 22744, 29652, 29276, 29415, 29602, 29430, 27204, 29285, 28931, 29211, 28864, 28859, 28593, 28952, 29121, 28529, 28158, 28457, 28355, 28129, 27816, 27028, 28771, 28650, 27372, 27119, 27659 ]); // Cache key $cache_key = 'nadcab_hire_pages_api_cache'; $cached_data = get_transient($cache_key); if ($cached_data !== false) { return rest_ensure_response($cached_data); } // Query pages $pages = get_posts([ 'post_type' => ['page', 'hire', 'services'], 'post__in' => $include_ids, 'posts_per_page' => count($include_ids), 'orderby' => 'post__in', 'order' => 'ASC', 'post_status' => 'publish', 'suppress_filters' => false, ]); if (empty($pages)) { return rest_ensure_response(['message' => 'No selected hire pages found']); } $data = []; foreach ($pages as $page) { $page_id = $page->ID; // Basic fields $title = get_the_title($page_id); $slug = sanitize_title($page->post_name); $date = get_the_date('Y-m-d H:i:s', $page_id); $link = esc_url(get_permalink($page_id)); $excerpt = wp_trim_words(wp_strip_all_tags($page->post_content), 50); $content = apply_filters('the_content', $page->post_content); // Featured image $thumb_url = get_the_post_thumbnail_url($page_id, 'full'); $thumb_alt = get_post_meta(get_post_thumbnail_id($page_id), '_wp_attachment_image_alt', true); // ACF fields (expanded images) $acf_fields = function_exists('get_fields') ? get_fields($page_id) : []; $acf_fields = nadcab_expand_acf_images_recursive($acf_fields); $acf_fields = nadcab_clean_acf_text($acf_fields); // Prepare SEO container $seo = [ 'title' => get_post_meta($page_id, 'rank_math_title', true) ?: $title, 'description' => get_post_meta($page_id, 'rank_math_description', true) ?: $excerpt, 'focus' => get_post_meta($page_id, 'rank_math_focus_keyword', true), 'canonical' => get_post_meta($page_id, 'rank_math_canonical_url', true) ?: $link, // fill in below: meta_tags, og, twitter, schema 'meta_tags' => [], 'og' => [], 'twitter' => [], 'schema' => [], ]; // ------------------------- // Extract Rank Math Meta (render meta HTML in page context and parse) // ------------------------- if (defined('RANK_MATH_VERSION') && function_exists('rank_math')) { global $wp_query; $old_query = $wp_query; // Temporarily set a query for this single page so Rank Math outputs correct meta $wp_query = new WP_Query(['p' => $page_id, 'post_type' => $page->post_type]); // Capture meta tags (OG, Twitter) by calling Rank Math frontend methods ob_start(); try { if (isset(rank_math()->frontend) && method_exists(rank_math()->frontend, 'get_meta_tags')) { echo rank_math()->frontend->get_meta_tags(); } elseif (isset(rank_math()->frontend) && method_exists(rank_math()->frontend, 'head')) { echo rank_math()->frontend->head(); } else { // fallback to standard wp_head() if Rank Math hooks into it do_action('wp_head'); } $meta_html = ob_get_clean(); } catch (Throwable $e) { ob_end_clean(); $meta_html = ''; } // parse meta tags like and if ($meta_html && preg_match_all('/]+>/i', $meta_html, $matches)) { foreach ($matches[0] as $tag) { // property="og:..." if (preg_match('/property=["\']([^"\']+)["\'].*content=["\']([^"\']+)["\']/', $tag, $m)) { $prop = $m[1]; $cont = html_entity_decode($m[2]); // OG if (strpos($prop, 'og:') === 0) { $og_key = substr($prop, 3); $seo['og'][$og_key] = $cont; $seo['meta_tags']["og:$og_key"] = $cont; } } // name="twitter:..." elseif (preg_match('/name=["\']([^"\']+)["\'].*content=["\']([^"\']+)["\']/', $tag, $m2)) { $name = $m2[1]; $cont = html_entity_decode($m2[2]); if (strpos($name, 'twitter:') === 0) { $tw_key = substr($name, 8); $seo['twitter'][$tw_key] = $cont; $seo['meta_tags']["twitter:$tw_key"] = $cont; } else { // generic meta name (e.g. description) $seo['meta_tags'][$name] = $cont; } } else { // also capture itemprop or other meta formats (content attr) if (preg_match('/content=["\']([^"\']+)["\']/', $tag, $m3) && preg_match('/(property|name|itemprop)=["\']([^"\']+)["\']/', $tag, $m4)) { $propname = $m4[2]; $cont = html_entity_decode($m3[1]); $seo['meta_tags'][$propname] = $cont; } } } } // Capture Rank Math JSON-LD schema output ob_start(); try { if (function_exists('rank_math_the_json_ld')) { rank_math_the_json_ld(); } elseif (isset(rank_math()->json) && method_exists(rank_math()->json, 'get_schemas')) { // some versions have a get_schemas method $schemas = rank_math()->json->get_schemas($page_id); if (!empty($schemas)) { // ensure array $seo['schema'] = $schemas; } } $schema_html = ob_get_clean(); } catch (Throwable $e) { ob_end_clean(); $schema_html = ''; } // parse if ($schema_html && preg_match_all('/]*type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is', $schema_html, $schema_matches)) { foreach ($schema_matches[1] as $schema_json) { $decoded = json_decode(trim($schema_json), true); if ($decoded) { $seo['schema'][] = $decoded; } else { // if invalid JSON, keep raw string as last resort $seo['schema'][] = trim($schema_json); } } } // restore global query $wp_query = $old_query; wp_reset_postdata(); } else { // If Rank Math not defined, try fallback reading meta tags from post meta (best-effort) $meta_keys = get_post_meta($page_id); foreach ($meta_keys as $k => $v) { $val = is_array($v) ? reset($v) : $v; // common Rank Math meta keys (best-effort) if (strpos($k, 'rank_math_og_') === 0) { $seo['og'][str_replace('rank_math_og_', '', $k)] = $val; } if (strpos($k, 'rank_math_twitter_') === 0) { $seo['twitter'][str_replace('rank_math_twitter_', '', $k)] = $val; } } } // If OG image missing, fallback to featured image if (empty($seo['og']) || empty($seo['og']['image'])) { if ($thumb_url) { $seo['og']['image'] = $thumb_url; $seo['meta_tags']['og:image'] = $thumb_url; } } // Compose final item $item = [ 'id' => $page_id, 'title' => $title, 'slug' => $slug, 'date' => $date, 'link' => $link, 'featured_image' => [ 'url' => $thumb_url ?: '', 'alt' => $thumb_alt ?: '', ], 'excerpt' => $excerpt, 'content' => $content, 'acf' => $acf_fields, 'seo' => $seo, ]; $data[] = $item; } // Cache 10 minutes set_transient($cache_key, $data, 10 * MINUTE_IN_SECONDS); return rest_ensure_response($data); } /* ------------------------------------------------------------------------- Helper: Recursively expand ACF image IDs into objects with URL/alt/title --------------------------------------------------------------------------- */ function nadcab_expand_acf_images_recursive($data) { if (is_array($data)) { foreach ($data as $k => $v) { // nested array if (is_array($v)) { $data[$k] = nadcab_expand_acf_images_recursive($v); } else { // if it's numeric and an attachment ID -> expand if (is_numeric($v) && get_post_type($v) === 'attachment') { $att_id = intval($v); $meta = wp_get_attachment_metadata($att_id); $data[$k] = [ 'id' => $att_id, 'url' => wp_get_attachment_url($att_id), 'alt' => get_post_meta($att_id, '_wp_attachment_image_alt', true), 'title' => get_the_title($att_id), 'width' => isset($meta['width']) ? $meta['width'] : '', 'height' => isset($meta['height']) ? $meta['height'] : '', 'mime_type' => get_post_mime_type($att_id), ]; } else { // leave as-is $data[$k] = $v; } } } } return $data; } /* ------------------------------------------------------------------------- Helper: Clean ACF strings that contain escaped HTML entities like \u003C --------------------------------------------------------------------------- */ function nadcab_clean_acf_text($data) { if (is_array($data)) { foreach ($data as $k => $v) { $data[$k] = nadcab_clean_acf_text($v); } return $data; } if (is_string($data)) { // decode common unicode escapes & html entities $data = preg_replace_callback('/\\\\u([0-9a-fA-F]{4})/', function($m){ return mb_convert_encoding(pack('H*', $m[1]), 'UTF-8', 'UCS-2BE'); }, $data); $data = html_entity_decode($data, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } return $data; }