wp_remote_get // timeout (15s) so a dying request can't hold it indefinitely. $lock_key = $cache_key . '_lock'; $we_hold_lock = false; if ( false === get_transient( $lock_key ) ) { set_transient( $lock_key, time(), 20 ); $we_hold_lock = true; } else { $stale = get_option( 'a1tools_stale_' . $cache_key ); if ( $stale ) { $request_cache[ $cache_key ] = $stale; return $stale; } // First-ever request: no stale yet, lock-holder still working. // Fall through and fetch ourselves — small race is better than // blocking the page render indefinitely. } try { $response = wp_remote_get( $api_url, array( 'timeout' => 15, 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'A1 Tools API error (' . $cache_key . '): ' . $response->get_error_message() ); } // Negative cache: prevent hammering a down service. set_transient( $cache_key . '_fail', time(), 60 ); $stale = get_option( 'a1tools_stale_' . $cache_key ); $result = $stale ?: null; $request_cache[ $cache_key ] = $result; return $result; } $code = wp_remote_retrieve_response_code( $response ); if ( $code < 200 || $code >= 300 ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'A1 Tools API HTTP ' . $code . ' (' . $cache_key . ')' ); } set_transient( $cache_key . '_fail', time(), 60 ); $stale = get_option( 'a1tools_stale_' . $cache_key ); $result = $stale ?: null; $request_cache[ $cache_key ] = $result; return $result; } $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( ! is_array( $data ) || empty( $data['success'] ) ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'A1 Tools API invalid response (' . $cache_key . ')' ); } set_transient( $cache_key . '_fail', time(), 60 ); $stale = get_option( 'a1tools_stale_' . $cache_key ); $result = $stale ?: null; $request_cache[ $cache_key ] = $result; return $result; } $result = $data[ $data_key ] ?? array(); if ( $filter_name ) { $result = apply_filters( $filter_name, $result ); } set_transient( $cache_key, $result, $cache_expiry ); // Save as stale fallback (used only when the API is unreachable). update_option( 'a1tools_stale_' . $cache_key, $result, false ); $request_cache[ $cache_key ] = $result; return $result; } finally { if ( $we_hold_lock ) { delete_transient( $lock_key ); } } } /** * Build the standard A1 Tools public API URL. * * @since 2.2.1 * * @param string $filter_name WordPress filter for the base URL. * @param string $default_url Default API base URL constant. * @return string Full API URL with action and url query args. */ function a1tools_build_api_url( $filter_name, $default_url ) { $api_base_url = apply_filters( $filter_name, $default_url ); return add_query_arg( array( 'action' => 'public', 'url' => site_url(), ), $api_base_url ); } /** * Get site variables from API or cache. * * @since 1.0.0 * @param bool $force_refresh Force a fresh API call. * @return array|null Variables array or null on error. */ function a1tools_get_site_variables( $force_refresh = false, $contextual = true ) { $url = a1tools_build_api_url( 'a1tools_api_url', A1TOOLS_DEFAULT_API_URL ); $variables = a1tools_fetch_api_data( A1TOOLS_CACHE_KEY, $url, 'variables', '', $force_refresh ); // Context-aware overlay: on a City or Location page, that page's own values // (phone, address, city, state) take precedence over the main-phase values, // so a single base widget/shortcode (e.g. [a1tools_phone], the A1 Phone // widget) shows the page-specific value and falls back to the main value // wherever the page leaves a field blank. Opt out ($contextual = false) for // admin previews and the raw REST endpoint, which must stay site-global. if ( $contextual && is_array( $variables ) ) { $overrides = a1tools_get_page_field_overrides(); if ( ! empty( $overrides ) ) { $variables = array_merge( $variables, $overrides ); } } return $variables; } /** * Field-override map for the current WordPress page (context-aware widgets). * * A page is exclusively a City page OR a Location page — the two are built * under different author accounts ("Single City" vs "Single Location") and * never share a page, so there is no ambiguity: the overrides come from * whichever dataset (if either) owns the current page. Only non-empty page * values are returned, so any field the page leaves blank falls back to the * main-phase value via array_merge() in a1tools_get_site_variables(). * * Keys are main-phase variable keys, so the result overlays the variables * array directly: phone_primary, city, state, full_address, address_line1, * address_line2, zip, country. * * @since 2.8.0 * @return array Associative array of overrides (may be empty). */ function a1tools_get_page_field_overrides() { static $cache = array(); $page_id = intval( get_the_ID() ); if ( $page_id <= 0 ) { return array(); } if ( isset( $cache[ $page_id ] ) ) { return $cache[ $page_id ]; } $overrides = array(); // City pages carry a full address breakdown (line1/line2/city/state/zip). $city_pages = function_exists( 'a1tools_get_site_city_pages' ) ? a1tools_get_site_city_pages() : null; if ( is_array( $city_pages ) && isset( $city_pages[ $page_id ] ) ) { $cp = $city_pages[ $page_id ]; a1tools_add_override( $overrides, 'phone_primary', $cp, 'phone' ); a1tools_add_override( $overrides, 'city', $cp, 'city' ); a1tools_add_override( $overrides, 'state', $cp, 'state' ); a1tools_add_override( $overrides, 'address_line1', $cp, 'address_line1' ); a1tools_add_override( $overrides, 'address_line2', $cp, 'address_line2' ); a1tools_add_override( $overrides, 'zip', $cp, 'zip' ); a1tools_add_override( $overrides, 'country', $cp, 'country' ); // Keep full_address consistent with the parts for widgets that read it. $full = a1tools_assemble_address( $cp ); if ( '' !== $full ) { $overrides['full_address'] = $full; } $cache[ $page_id ] = $overrides; return $overrides; } // Location pages carry a single stored full address plus city/state. $locations = function_exists( 'a1tools_get_seo_locations' ) ? a1tools_get_seo_locations() : null; if ( is_array( $locations ) && isset( $locations[ $page_id ] ) ) { $lp = $locations[ $page_id ]; a1tools_add_override( $overrides, 'phone_primary', $lp, 'phone' ); a1tools_add_override( $overrides, 'city', $lp, 'city' ); a1tools_add_override( $overrides, 'state', $lp, 'state' ); a1tools_add_override( $overrides, 'full_address', $lp, 'full_address' ); } $cache[ $page_id ] = $overrides; return $overrides; } /** * Copy a source field into the overrides array only when it is non-empty. * * @since 2.8.0 * @param array $out Overrides array (by reference). * @param string $out_key Destination (main-phase) variable key. * @param array $src Source page-data row. * @param string $src_key Source column name. * @return void */ function a1tools_add_override( &$out, $out_key, $src, $src_key ) { if ( isset( $src[ $src_key ] ) && '' !== trim( (string) $src[ $src_key ] ) ) { $out[ $out_key ] = $src[ $src_key ]; } } /** * Assemble a single-line address ("123 Main St, Suite 2, Stamford, CT 06901") * from a city-page row's individual parts. * * @since 2.8.0 * @param array $row City-page data row. * @return string Assembled address, or '' when no parts are present. */ function a1tools_assemble_address( $row ) { $line1 = ! empty( $row['address_line1'] ) ? $row['address_line1'] : ''; $line2 = ! empty( $row['address_line2'] ) ? $row['address_line2'] : ''; $city = ! empty( $row['city'] ) ? $row['city'] : ''; $state = ! empty( $row['state'] ) ? $row['state'] : ''; $zip = ! empty( $row['zip'] ) ? $row['zip'] : ''; $parts = array(); if ( '' !== $line1 ) { $parts[] = $line1; } if ( '' !== $line2 ) { $parts[] = $line2; } $tail = trim( $city . ( '' !== $state ? ', ' . $state : '' ) . ( '' !== $zip ? ' ' . $zip : '' ) ); if ( '' !== $tail ) { $parts[] = $tail; } return implode( ', ', $parts ); } /** * Auto-resolve the CRM company id for THIS WordPress site. * * Returns the value stored in `a1tools_form_crm_company_id` when it's * non-zero (admin override wins). Otherwise hits * `website_variables.php?action=public&url=` once, reads * `company_id` off the root of the response, and persists it back * into the option so the Web Chat + Web Form widgets render with the * correct `data-company-id` without any admin intervention. * * The remote response shape (since A1 Tools 5.3.171) is: * { success: true, variables: {...}, franchises: [...], group_id: N, company_id: N } * * Returns 0 when the resolution fails (DNS error, group not bound to * a CRM company, etc.). The widgets gate on the integer being > 0 * before mounting, so a 0 here keeps the previous "no config" UX. * * Result is briefly negative-cached (10 min) so a failure doesn't * hammer the API on every page render. * * @since 2.5.6 * @return int CRM company id (>0) or 0 when unresolved. */ function a1tools_resolve_crm_company_id() { $current = (int) get_option( 'a1tools_form_crm_company_id', 0 ); if ( $current > 0 ) { return $current; } // Negative cache — don't hammer the API on every page request // when the site's group simply isn't bound to a CRM company yet. $neg_key = 'a1tools_crm_company_id_resolve_fail'; if ( get_transient( $neg_key ) ) { return 0; } $api_url = a1tools_build_api_url( 'a1tools_api_url', A1TOOLS_DEFAULT_API_URL ); $response = wp_remote_get( $api_url, array( 'timeout' => 10, 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { set_transient( $neg_key, time(), 10 * MINUTE_IN_SECONDS ); return 0; } $code = wp_remote_retrieve_response_code( $response ); if ( $code < 200 || $code >= 300 ) { set_transient( $neg_key, time(), 10 * MINUTE_IN_SECONDS ); return 0; } $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( ! is_array( $data ) || empty( $data['success'] ) || ! isset( $data['company_id'] ) ) { set_transient( $neg_key, time(), 10 * MINUTE_IN_SECONDS ); return 0; } $cid = (int) $data['company_id']; if ( $cid > 0 ) { // Persist so subsequent renders don't re-fetch. The admin // settings page still shows + accepts manual edits — this // just back-fills the empty default. update_option( 'a1tools_form_crm_company_id', $cid, false ); // Also clear the negative cache in case it was set earlier. delete_transient( $neg_key ); return $cid; } set_transient( $neg_key, time(), 10 * MINUTE_IN_SECONDS ); return 0; } /** * Get a single site variable. * * @since 1.0.0 * @param string $key Variable key (e.g., 'phone_primary', 'facebook_url'). * @param string $default Default value if not found. * @return string Variable value. */ function a1tools_get_variable( $key, $default = '' ) { $variables = a1tools_get_site_variables(); if ( null === $variables || ! isset( $variables[ $key ] ) ) { return $default; } return $variables[ $key ]; } /** * Clear the site variables cache. * * @since 1.0.0 * @return void */ function a1tools_clear_cache() { delete_transient( A1TOOLS_CACHE_KEY ); delete_transient( A1TOOLS_STORES_CACHE_KEY ); delete_transient( A1TOOLS_FRANCHISES_CACHE_KEY ); delete_transient( A1TOOLS_CITY_PAGES_CACHE_KEY ); delete_transient( A1TOOLS_SERVICES_CACHE_KEY ); delete_transient( A1TOOLS_REVIEWS_CACHE_KEY ); delete_transient( A1TOOLS_BEFORE_AFTER_CACHE_KEY ); delete_transient( A1TOOLS_TEAM_CACHE_KEY ); delete_transient( A1TOOLS_FAQ_CACHE_KEY ); if ( defined( 'A1TOOLS_SEO_LOCATIONS_CACHE_KEY' ) ) { delete_transient( A1TOOLS_SEO_LOCATIONS_CACHE_KEY ); } if ( defined( 'A1TOOLS_CONTENT_BLOCKS_CACHE_KEY' ) ) { delete_transient( A1TOOLS_CONTENT_BLOCKS_CACHE_KEY ); } } /** * Get group store locations from the A1 Tools API. * * Fetches store data for the group that this site belongs to. * Results are cached using transients. * * @since 1.7.0 * @param bool $force_refresh Whether to bypass cache. * @return array|null Array of stores or null on failure. */ function a1tools_get_group_stores( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_stores_api_url', A1TOOLS_DEFAULT_STORES_API_URL ); return a1tools_fetch_api_data( A1TOOLS_STORES_CACHE_KEY, $url, 'stores', '', $force_refresh ); } /** * Get franchise locations from the A1 Tools API. * * Fetches franchise data for this specific site. * Results are cached using transients. * * @since 1.7.1 * @param bool $force_refresh Whether to bypass cache. * @return array|null Array of franchises or null on failure. */ function a1tools_get_site_franchises( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_franchises_api_url', A1TOOLS_DEFAULT_FRANCHISES_API_URL ); return a1tools_fetch_api_data( A1TOOLS_FRANCHISES_CACHE_KEY, $url, 'franchises', '', $force_refresh ); } /** * Get city page data from the A1 Tools API. * * Fetches city-specific phone/address data for this site's WordPress city pages. * Results are cached using transients and keyed by wp_page_id for O(1) shortcode lookup. * * @since 1.7.9 * @param bool $force_refresh Whether to bypass cache. * @return array|null Associative array keyed by wp_page_id, or null on failure. */ function a1tools_get_site_city_pages( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_city_pages_api_url', A1TOOLS_DEFAULT_CITY_PAGES_API_URL ); return a1tools_fetch_api_data( A1TOOLS_CITY_PAGES_CACHE_KEY, $url, 'city_pages', '', $force_refresh ); } /** * Get reusable content blocks from the A1 Tools API. * * Site-wide content snippets authored in the SEO app, keyed by slug for O(1) * lookup by the [a1tools_content] shortcode. Each entry is { label, content }. * * @since 3.1.0 * @param bool $force_refresh Whether to bypass cache. * @return array|null Associative array keyed by slug, or null on failure. */ function a1tools_get_site_content_blocks( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_content_blocks_api_url', A1TOOLS_DEFAULT_CONTENT_BLOCKS_API_URL ); return a1tools_fetch_api_data( A1TOOLS_CONTENT_BLOCKS_CACHE_KEY, $url, 'content_blocks', '', $force_refresh ); } /** * Get ALL WordPress city pages by querying the local WP database directly. * * City / location pages are identified by author display names containing * "city" OR "location" (e.g., "Single City", "Location City", * "Single Location"), excluding "SEO City" authors. Both naming * conventions are accepted because different customer sites were set up * over time with different conventions for the location-author user * (ct.a-1chimney.com uses "Single Location"; atl.a-1chimney.com uses * "Location City"). This returns every published city/location page * regardless of whether it has saved data in the A1 Tools API. * * Uses $wpdb direct queries and WP_Query for maximum reliability across * all WordPress versions and configurations. * * @since 1.8.2 * @updated 2.5.9 Match "location" alongside "city" so the City List * widget surfaces pages from "Single Location" authors * in addition to "Location City" / "Single City" * authors. Mirrors the broadening applied server-side * in api/site_city_pages.php::handleWpPages(). * @return array Array of city pages keyed by WP page ID. */ function a1tools_get_wp_city_pages() { static $cached = null; if ( null !== $cached ) { return $cached; } global $wpdb; // Direct DB query for city/location authors — most reliable across // all WP versions. Match display_name containing "city" OR "location" // but NOT "seo" (excludes "SEO City" authors which are handled by a // dedicated helper). The page-type detection further down keys off // "location" in the name, so the type classification (location_city // vs single_city) still works correctly regardless of which keyword // matched. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- cached via static variable above. $authors = $wpdb->get_results( $wpdb->prepare( "SELECT ID, display_name FROM {$wpdb->users} WHERE (LOWER(display_name) LIKE %s OR LOWER(display_name) LIKE %s) AND LOWER(display_name) NOT LIKE %s", '%city%', '%location%', '%seo%' ) ); if ( empty( $authors ) ) { $cached = array(); return $cached; } $city_author_ids = array(); $author_names = array(); foreach ( $authors as $author ) { $city_author_ids[] = (int) $author->ID; $author_names[ (int) $author->ID ] = $author->display_name; } // Use WP_Query for pages — handles large result sets reliably. $query = new WP_Query( array( 'post_type' => 'page', 'author__in' => $city_author_ids, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => true, // Skip counting for performance. ) ); $result = array(); if ( $query->have_posts() ) { foreach ( $query->posts as $page ) { $author_id = (int) $page->post_author; $author_name = isset( $author_names[ $author_id ] ) ? $author_names[ $author_id ] : ''; $page_type = ( false !== stripos( $author_name, 'location' ) ) ? 'location_city' : 'single_city'; $result[ $page->ID ] = array( 'wp_page_id' => $page->ID, 'page_title' => $page->post_title, 'page_type' => $page_type, 'author_name' => $author_name, 'group_name' => null, 'group_order' => 0, ); } } wp_reset_postdata(); $cached = $result; return $cached; } /** * Get ALL WordPress service pages by querying the local WP database directly. * * Service pages are identified by author display names containing "large service" * (e.g., "Large Service"), excluding "SEO" authors. * This returns every published service page regardless of whether it has * saved data in the A1 Tools API. * * @since 1.9.0 * @return array Array of service pages keyed by WP page ID. */ function a1tools_get_wp_service_pages() { static $cached = null; if ( null !== $cached ) { return $cached; } global $wpdb; // Direct DB query for service authors — most reliable across all WP versions. // Match display_name containing "large service" but NOT "seo". // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- cached via static variable above. $authors = $wpdb->get_results( $wpdb->prepare( "SELECT ID, display_name FROM {$wpdb->users} WHERE LOWER(display_name) LIKE %s AND LOWER(display_name) NOT LIKE %s", '%large service%', '%seo%' ) ); if ( empty( $authors ) ) { $cached = array(); return $cached; } $service_author_ids = array(); $author_names = array(); foreach ( $authors as $author ) { $service_author_ids[] = (int) $author->ID; $author_names[ (int) $author->ID ] = $author->display_name; } // Use WP_Query for pages — handles large result sets reliably. $query = new WP_Query( array( 'post_type' => 'page', 'author__in' => $service_author_ids, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => true, // Skip counting for performance. ) ); $result = array(); if ( $query->have_posts() ) { foreach ( $query->posts as $page ) { $author_id = (int) $page->post_author; $author_name = isset( $author_names[ $author_id ] ) ? $author_names[ $author_id ] : ''; $result[ $page->ID ] = array( 'wp_page_id' => $page->ID, 'page_title' => $page->post_title, 'author_name' => $author_name, ); } } wp_reset_postdata(); $cached = $result; return $cached; } /** * Get service data from the A1 Tools API. * * Fetches service-specific data for this site's WordPress service pages. * Results are cached using transients and keyed by wp_page_id for O(1) shortcode lookup. * * @since 1.9.0 * @param bool $force_refresh Whether to bypass cache. * @return array|null Associative array keyed by wp_page_id, or null on failure. */ function a1tools_get_site_services( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_services_api_url', A1TOOLS_DEFAULT_SERVICES_API_URL ); return a1tools_fetch_api_data( A1TOOLS_SERVICES_CACHE_KEY, $url, 'services', '', $force_refresh ); } /** * Get group reviews from the A1 Tools API. * * Fetches review data for the group that this site belongs to. * Results are cached using transients. * * @since 1.9.0 * @param bool $force_refresh Whether to bypass cache. * @return array|null Array of reviews or null on failure. */ function a1tools_get_group_reviews( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_reviews_api_url', A1TOOLS_DEFAULT_REVIEWS_API_URL ); return a1tools_fetch_api_data( A1TOOLS_REVIEWS_CACHE_KEY, $url, 'reviews', '', $force_refresh ); } /** * Get group before/after items from the A1 Tools API. * * Fetches before/after comparison data for the group that this site belongs to. * Results are cached using transients. * * @since 1.9.0 * @param bool $force_refresh Whether to bypass cache. * @return array|null Array of before/after items or null on failure. */ function a1tools_get_group_before_after( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_before_after_api_url', A1TOOLS_DEFAULT_BEFORE_AFTER_API_URL ); return a1tools_fetch_api_data( A1TOOLS_BEFORE_AFTER_CACHE_KEY, $url, 'items', '', $force_refresh ); } /** * Get group team members from the A1 Tools API. * * Fetches team member data for the group that this site belongs to. * Results are cached using transients. * * @since 1.9.0 * @param bool $force_refresh Whether to bypass cache. * @return array|null Array of team members or null on failure. */ function a1tools_get_group_team( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_team_api_url', A1TOOLS_DEFAULT_TEAM_API_URL ); return a1tools_fetch_api_data( A1TOOLS_TEAM_CACHE_KEY, $url, 'members', '', $force_refresh ); } /** * Get group FAQ items from the A1 Tools API. * * Fetches FAQ data for the group that this site belongs to. * Results are cached using transients. * * @since 1.9.0 * @param bool $force_refresh Whether to bypass cache. * @return array|null Array of FAQ items or null on failure. */ function a1tools_get_group_faq( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_faq_api_url', A1TOOLS_DEFAULT_FAQ_API_URL ); return a1tools_fetch_api_data( A1TOOLS_FAQ_CACHE_KEY, $url, 'faqs', '', $force_refresh ); } /** * Get SEO location data from the A1 Tools API. * * Fetches SEO-specific location data (phone, address, web URLs, map, viewpoint) * for this site's WordPress SEO city pages. * Results are cached using transients and keyed by wp_page_id for O(1) shortcode lookup. * * @since 2.1.0 * @param bool $force_refresh Whether to bypass cache. * @return array|null Associative array keyed by wp_page_id, or null on failure. */ function a1tools_get_seo_locations( $force_refresh = false ) { $url = a1tools_build_api_url( 'a1tools_seo_locations_api_url', A1TOOLS_DEFAULT_SEO_LOCATIONS_API_URL ); return a1tools_fetch_api_data( A1TOOLS_SEO_LOCATIONS_CACHE_KEY, $url, 'seo_locations', '', $force_refresh ); } /** * Get ALL WordPress SEO city pages by querying the local WP database directly. * * SEO city pages are identified by author display names containing BOTH * 'seo' AND 'city' (e.g., "SEO City"). * This returns every published SEO city page regardless of whether it has * saved data in the A1 Tools API. * * Uses $wpdb direct queries and WP_Query for maximum reliability across * all WordPress versions and configurations. * * @since 2.1.0 * @return array Array of SEO city pages keyed by WP page ID. */ function a1tools_get_wp_seo_city_pages() { static $cached = null; if ( null !== $cached ) { return $cached; } global $wpdb; // Direct DB query for SEO city authors — match display_name containing // both "seo" and "city" (e.g., "SEO City"). // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- cached via static variable above. $authors = $wpdb->get_results( $wpdb->prepare( "SELECT ID, display_name FROM {$wpdb->users} WHERE LOWER(display_name) LIKE %s AND LOWER(display_name) LIKE %s", '%seo%', '%city%' ) ); if ( empty( $authors ) ) { $cached = array(); return $cached; } $seo_author_ids = array(); $author_names = array(); foreach ( $authors as $author ) { $seo_author_ids[] = (int) $author->ID; $author_names[ (int) $author->ID ] = $author->display_name; } // Use WP_Query for pages — handles large result sets reliably. $query = new WP_Query( array( 'post_type' => 'page', 'author__in' => $seo_author_ids, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => true, // Skip counting for performance. ) ); $result = array(); if ( $query->have_posts() ) { foreach ( $query->posts as $page ) { $author_id = (int) $page->post_author; $author_name = isset( $author_names[ $author_id ] ) ? $author_names[ $author_id ] : ''; $result[ $page->ID ] = array( 'wp_page_id' => $page->ID, 'page_title' => $page->post_title, 'author_name' => $author_name, 'group_name' => null, 'group_order' => 0, ); } } wp_reset_postdata(); $cached = $result; return $cached; }