📁 File Manager Pro
v10.0.3 | PHP: 8.1.34
Server: LiteSpeed
2026-06-29 01:02:32
📂
/ (Root)
/
home
/
apkbfjox
/
x03bet.cloud
/
wp-content
/
plugins
/
seo-by-rank-math
/
includes
/
modules
/
links
/
Api
📍 /home/apkbfjox/x03bet.cloud/wp-content/plugins/seo-by-rank-math/includes/modules/links/Api
🔄 Refresh
✏️
Editing: class-controller.php
Writable
<?php /** * Links REST API controller. * * Handles links-related endpoints for the Free plugin admin page: * /posts, /posts-stats, /links, /links-stats. * * @since 1.0.266 * @package RankMath * @subpackage RankMath\Links\Api * @author Rank Math <support@rankmath.com> */ namespace RankMath\Links\Api; use WP_REST_Server; use WP_REST_Request; use WP_REST_Response; use RankMath\Helper; defined( 'ABSPATH' ) || exit; /** * Controller class. * * Provides REST endpoints for basic links and posts data using * only Free plugin tables (rank_math_internal_meta, rank_math_internal_links). * PRO plugin can override responses via filters to supply richer data. */ class Controller { /** * REST API namespace. * * @var string */ const NAMESPACE = 'rankmath/v1'; /** * Cache group for object caching. * * @var string */ const CACHE_GROUP = 'rank_math_links'; /** * Cache TTL in seconds (30 minutes). * * @var int */ const CACHE_TTL = 1800; /** * Register REST API routes. */ public function register_routes() { // GET /rankmath/v1/links/posts. register_rest_route( self::NAMESPACE, '/links/posts', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_posts' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => $this->get_posts_args(), ] ); // GET /rankmath/v1/links/posts-stats. register_rest_route( self::NAMESPACE, '/links/posts-stats', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_posts_stats' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); // GET /rankmath/v1/links/links. register_rest_route( self::NAMESPACE, '/links/links', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_links' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => $this->get_links_args(), ] ); // GET /rankmath/v1/links/links-stats. register_rest_route( self::NAMESPACE, '/links/links-stats', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_links_stats' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); } /** * Permission callback: require manage_options capability. * * @return bool|\WP_Error */ public function check_permission() { if ( ! current_user_can( 'manage_options' ) ) { return new \WP_Error( 'rest_forbidden', esc_html__( 'You do not have permission to access this resource.', 'seo-by-rank-math' ), [ 'status' => rest_authorization_required_code() ] ); } return true; } /** * Get posts with link metrics. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public function get_posts( $request ) { $data = $this->get_posts_data( [ 'page' => $request->get_param( 'page' ), 'per_page' => $request->get_param( 'per_page' ), 'search' => $request->get_param( 'search' ), 'post_type' => $request->get_param( 'post_type' ), 'is_orphan' => $request->get_param( 'is_orphan' ), 'seo_score_range' => $request->get_param( 'seo_score_range' ), 'post_id' => $request->get_param( 'post_id' ), 'orderby' => $request->get_param( 'orderby' ), 'order' => $request->get_param( 'order' ), ] ); return rest_ensure_response( $data ); } /** * Query post link stats and return the data array. * * Shared by the REST endpoint and the Abilities API so neither needs to * construct a WP_REST_Request. The REST endpoint wraps the return value * in rest_ensure_response(); abilities call this method directly. * * @param array $args { * Raw or pre-sanitized query arguments. * * @type int $page 1-based page number. Default 1. * @type int $per_page Rows per page (max 100). Default 50. * @type string $search Partial post-title search. Default ''. * @type array $post_type Allowed post types. Default []. * @type string $is_orphan Orphan filter: 'orphan', 'linked', or ''. Default ''. * @type string $seo_score_range SEO score filter: great, good, bad, no-score, or ''. Default ''. * @type int $post_id Filter to a single post by ID. Default 0. * @type string $orderby Order-by field. Default 'post_title'. * @type string $order Sort direction: ASC or DESC. Default 'ASC'. * } * @return array { posts: array, total: int, pages: int } */ public function get_posts_data( array $args ): array { $page = max( 1, (int) ( $args['page'] ?? 1 ) ); $per_page = min( 100, max( 1, (int) ( $args['per_page'] ?? 50 ) ) ); $offset = ( $page - 1 ) * $per_page; $normalized = [ 'search' => sanitize_text_field( (string) ( $args['search'] ?? '' ) ), 'post_type' => (array) ( $args['post_type'] ?? [] ), 'is_orphan' => sanitize_text_field( (string) ( $args['is_orphan'] ?? '' ) ), 'seo_score_range' => sanitize_text_field( (string) ( $args['seo_score_range'] ?? '' ) ), 'post_id' => absint( $args['post_id'] ?? 0 ), 'orderby' => sanitize_text_field( (string) ( $args['orderby'] ?? 'post_title' ) ), 'order' => sanitize_text_field( (string) ( $args['order'] ?? 'ASC' ) ), 'offset' => $offset, 'per_page' => $per_page, ]; /** * Filter to allow overriding the posts REST response. * * Return a non-null array to bypass the Free plugin's query entirely. * Expected format: [ 'posts' => array, 'total' => int, 'pages' => int ] * * @since 1.0.266 * * @param null|array $override Return override data or null to use Free's query. * @param array $normalized Normalized query arguments. */ $override = apply_filters( 'rank_math/links/rest_posts_response', null, $normalized ); if ( null !== $override ) { return (array) $override; } $results = $this->query_posts( $normalized ); $total = $this->query_posts_count( $normalized ); return [ 'posts' => $results, 'total' => (int) $total, 'pages' => (int) ceil( $total / $per_page ), ]; } /** * Get aggregate posts statistics. * * @return WP_REST_Response */ public function get_posts_stats() { /** * Filter to allow overriding the posts-stats REST response. * * @since 1.0.266 * * @param null|array $override Return override data or null to use Free's query. */ $override = apply_filters( 'rank_math/links/rest_posts_stats_response', null ); if ( null !== $override ) { return rest_ensure_response( $override ); } $cache_key = 'posts_stats'; $cached = wp_cache_get( $cache_key, self::CACHE_GROUP ); if ( false !== $cached ) { return rest_ensure_response( $cached ); } global $wpdb; $stats = $wpdb->get_row( "SELECT COUNT(DISTINCT m.object_id) as total_posts, SUM(CASE WHEN m.incoming_link_count IS NULL OR m.incoming_link_count = 0 THEN 1 ELSE 0 END) as orphan_posts, SUM(CASE WHEN m.internal_link_count > 0 THEN 1 ELSE 0 END) as posts_with_internal, SUM(CASE WHEN m.external_link_count > 0 THEN 1 ELSE 0 END) as posts_with_external FROM {$wpdb->prefix}rank_math_internal_meta m INNER JOIN {$wpdb->posts} p ON m.object_id = p.ID WHERE p.post_status = 'publish'" ); $response = [ 'total_posts' => (int) ( $stats->total_posts ?? 0 ), 'orphan_posts' => (int) ( $stats->orphan_posts ?? 0 ), 'posts_with_internal' => (int) ( $stats->posts_with_internal ?? 0 ), 'posts_with_external' => (int) ( $stats->posts_with_external ?? 0 ), ]; wp_cache_set( $cache_key, $response, self::CACHE_GROUP, self::CACHE_TTL ); return rest_ensure_response( $response ); } /** * Get links with basic columns. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public function get_links( $request ) { return rest_ensure_response( $this->get_links_data( [ 'page' => $request->get_param( 'page' ), 'per_page' => $request->get_param( 'per_page' ), 'search' => $request->get_param( 'search' ), 'source_id' => $request->get_param( 'source_id' ), 'target_post_id' => $request->get_param( 'target_post_id' ), 'is_internal' => $request->get_param( 'is_internal' ), 'orderby' => $request->get_param( 'orderby' ), 'order' => $request->get_param( 'order' ), ] ) ); } /** * Fetch and normalize link rows without REST plumbing. * * Shared by the REST endpoint and the Abilities API runner so neither * needs to construct a WP_REST_Request object. * * @param array $args { * Query arguments (all optional). * * @type int $page 1-based page number (default 1). * @type int $per_page Rows per page, capped at 100 (default 50). * @type string $search URL search string. * @type int $source_id Source post ID. * @type int $target_post_id Target post ID. * @type string $is_internal '1' = internal only, '0' = external only, '' = all. * @type string $orderby Column to sort by. * @type string $order Sort direction. * } * @return array { links: array, total: int, pages: int } */ public function get_links_data( array $args ): array { $page = max( 1, (int) ( $args['page'] ?? 1 ) ); $per_page = min( 100, max( 1, (int) ( $args['per_page'] ?? 50 ) ) ); $offset = ( $page - 1 ) * $per_page; $normalized = [ 'search' => sanitize_text_field( (string) ( $args['search'] ?? '' ) ), 'source_id' => absint( $args['source_id'] ?? 0 ), 'target_post_id' => absint( $args['target_post_id'] ?? 0 ), 'is_internal' => sanitize_text_field( (string) ( $args['is_internal'] ?? '' ) ), 'orderby' => sanitize_text_field( (string) ( $args['orderby'] ?? '' ) ), 'order' => sanitize_text_field( (string) ( $args['order'] ?? '' ) ), 'offset' => $offset, 'per_page' => $per_page, ]; /** * Filter to allow overriding the links REST response. * * Return a non-null array to bypass the Free plugin's query entirely. * Expected format: [ 'links' => array, 'total' => int, 'pages' => int ] * * @since 1.0.266 * * @param null|array $override Return override data or null to use Free's query. * @param array $normalized Normalized query arguments. */ $override = apply_filters( 'rank_math/links/rest_links_response', null, $normalized ); if ( null !== $override ) { return (array) $override; } $results = $this->query_links( $normalized ); $total = $this->query_links_count( $normalized ); return [ 'links' => $results, 'total' => (int) $total, 'pages' => (int) ceil( $total / $per_page ), ]; } /** * Get basic links statistics (total, internal, external). * * @return WP_REST_Response */ public function get_links_stats() { /** * Filter to allow overriding the links-stats REST response. * * @since 1.0.266 * * @param null|array $override Return override data or null to use Free's query. */ $override = apply_filters( 'rank_math/links/rest_links_stats_response', null ); if ( null !== $override ) { return rest_ensure_response( $override ); } $cache_key = 'links_stats'; $cached = wp_cache_get( $cache_key, self::CACHE_GROUP ); if ( false !== $cached ) { return rest_ensure_response( $cached ); } global $wpdb; $table = $wpdb->prefix . 'rank_math_internal_links'; $stats = $wpdb->get_row( "SELECT COUNT(*) as total, SUM(CASE WHEN type = 'internal' THEN 1 ELSE 0 END) as internal, SUM(CASE WHEN type = 'external' THEN 1 ELSE 0 END) as external FROM {$table}" ); $response = [ 'total' => (int) ( $stats->total ?? 0 ), 'internal' => (int) ( $stats->internal ?? 0 ), 'external' => (int) ( $stats->external ?? 0 ), ]; wp_cache_set( $cache_key, $response, self::CACHE_GROUP, self::CACHE_TTL ); return rest_ensure_response( $response ); } /** * Query posts with link metrics from Free tables. * * @param array $args Query arguments. * @return array */ private function query_posts( $args ) { $cache_key = 'posts_' . md5( wp_json_encode( $args ) ); $cached = wp_cache_get( $cache_key, self::CACHE_GROUP ); if ( false !== $cached ) { return $cached; } global $wpdb; $where_sql = $this->build_posts_where( $args ); $orderby = $this->validate_posts_orderby( $args['orderby'] ); $order = $this->validate_order( $args['order'], 'ASC' ); $limit_sql = $wpdb->prepare( 'LIMIT %d OFFSET %d', $args['per_page'], $args['offset'] ); $results = $wpdb->get_results( "SELECT p.ID as post_id, p.post_title, p.post_type, p.post_modified, m.internal_link_count, m.external_link_count, m.incoming_link_count, pm_score.meta_value as seo_score FROM {$wpdb->prefix}rank_math_internal_meta m INNER JOIN {$wpdb->posts} p ON m.object_id = p.ID LEFT JOIN {$wpdb->postmeta} pm_score ON p.ID = pm_score.post_id AND pm_score.meta_key = 'rank_math_seo_score' WHERE {$where_sql} ORDER BY {$orderby} {$order} {$limit_sql}" ); foreach ( $results as $row ) { $row->internal_link_count = (int) ( $row->internal_link_count ?? 0 ); $row->external_link_count = (int) ( $row->external_link_count ?? 0 ); $row->incoming_link_count = (int) ( $row->incoming_link_count ?? 0 ); $row->seo_score = $row->seo_score ? (int) $row->seo_score : 0; $row->is_orphan = 0 === $row->incoming_link_count; $row->edit_url = get_edit_post_link( $row->post_id, '&' ); $row->post_url = $this->get_relative_permalink( $row->post_id ); $post_type_obj = get_post_type_object( $row->post_type ); $row->post_type_label = $post_type_obj ? $post_type_obj->labels->singular_name : ucfirst( $row->post_type ); } wp_cache_set( $cache_key, $results, self::CACHE_GROUP, self::CACHE_TTL ); return $results; } /** * Count posts matching query arguments. * * @param array $args Query arguments. * @return int */ private function query_posts_count( $args ) { $cache_key = 'posts_count_' . md5( wp_json_encode( $args ) ); $cached = wp_cache_get( $cache_key, self::CACHE_GROUP ); if ( false !== $cached ) { return $cached; } global $wpdb; $where_sql = $this->build_posts_where( $args ); $joins = "INNER JOIN {$wpdb->posts} p ON m.object_id = p.ID"; if ( ! empty( $args['seo_score_range'] ) ) { $joins .= " LEFT JOIN {$wpdb->postmeta} pm_score ON p.ID = pm_score.post_id AND pm_score.meta_key = 'rank_math_seo_score'"; } $count = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT m.object_id) FROM {$wpdb->prefix}rank_math_internal_meta m {$joins} WHERE {$where_sql}" ); wp_cache_set( $cache_key, $count, self::CACHE_GROUP, self::CACHE_TTL ); return $count; } /** * Query links from Free tables. * * @param array $args Query arguments. * @return array */ private function query_links( $args ) { $cache_key = 'links_' . md5( wp_json_encode( $args ) ); $cached = wp_cache_get( $cache_key, self::CACHE_GROUP ); if ( false !== $cached ) { return $cached; } global $wpdb; $where_sql = $this->build_links_where( $args ); $orderby = $this->validate_links_orderby( $args['orderby'] ); $order = $this->validate_order( $args['order'], 'DESC' ); $limit_sql = $wpdb->prepare( 'LIMIT %d OFFSET %d', $args['per_page'], $args['offset'] ); $table = $wpdb->prefix . 'rank_math_internal_links'; $results = $wpdb->get_results( "SELECT l.id, l.url, l.post_id, l.target_post_id, l.type, sp.post_title as source_title, tp.post_title as target_title FROM {$table} l LEFT JOIN {$wpdb->posts} sp ON l.post_id = sp.ID LEFT JOIN {$wpdb->posts} tp ON l.target_post_id = tp.ID WHERE {$where_sql} ORDER BY {$orderby} {$order} {$limit_sql}" ); foreach ( $results as $row ) { $row->source_url = $this->get_relative_permalink( $row->post_id ); $row->source_edit_url = $row->post_id ? get_edit_post_link( $row->post_id, '&' ) : ''; $row->target_url = ( 'internal' === $row->type ) ? $this->get_relative_permalink( $row->target_post_id ) : ''; } wp_cache_set( $cache_key, $results, self::CACHE_GROUP, self::CACHE_TTL ); return $results; } /** * Count links matching query arguments. * * @param array $args Query arguments. * @return int */ private function query_links_count( $args ) { $cache_key = 'links_count_' . md5( wp_json_encode( $args ) ); $cached = wp_cache_get( $cache_key, self::CACHE_GROUP ); if ( false !== $cached ) { return $cached; } global $wpdb; $table = $wpdb->prefix . 'rank_math_internal_links'; $where_sql = $this->build_links_where( $args ); $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table} l WHERE {$where_sql}" ); wp_cache_set( $cache_key, $count, self::CACHE_GROUP, self::CACHE_TTL ); return $count; } /** * Build WHERE clause for posts query. * * @param array $args Query arguments. * @return string */ private function build_posts_where( $args ) { global $wpdb; $where = [ "p.post_status = 'publish'" ]; // Post type filter. $post_types = array_filter( array_map( 'sanitize_text_field', (array) $args['post_type'] ) ); if ( ! empty( $post_types ) ) { $placeholders = implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare $where[] = $wpdb->prepare( 'p.post_type IN (' . $placeholders . ')', ...$post_types ); } else { $where[] = "p.post_type IN ('post', 'page')"; } // Orphan filter. if ( isset( $args['is_orphan'] ) && '' !== $args['is_orphan'] ) { if ( 'orphan' === $args['is_orphan'] ) { $where[] = '(m.incoming_link_count IS NULL OR m.incoming_link_count = 0)'; } elseif ( 'linked' === $args['is_orphan'] ) { $where[] = 'm.incoming_link_count > 0'; } } // SEO score range filter. if ( ! empty( $args['seo_score_range'] ) ) { switch ( $args['seo_score_range'] ) { case 'great': $where[] = 'CAST(pm_score.meta_value AS UNSIGNED) > 80'; break; case 'good': $where[] = 'CAST(pm_score.meta_value AS UNSIGNED) BETWEEN 51 AND 80'; break; case 'bad': $where[] = 'CAST(pm_score.meta_value AS UNSIGNED) <= 50'; break; case 'no-score': $where[] = "(pm_score.meta_value IS NULL OR pm_score.meta_value = '')"; break; } } // Post ID filter. if ( ! empty( $args['post_id'] ) ) { $where[] = $wpdb->prepare( 'p.ID = %d', $args['post_id'] ); } // Search filter. if ( ! empty( $args['search'] ) ) { $where[] = $wpdb->prepare( 'p.post_title LIKE %s', '%' . $wpdb->esc_like( $args['search'] ) . '%' ); } return implode( ' AND ', $where ); } /** * Build WHERE clause for links query. * * @param array $args Query arguments. * @return string */ private function build_links_where( $args ) { global $wpdb; $where = [ '1=1' ]; if ( ! empty( $args['source_id'] ) ) { $where[] = $wpdb->prepare( 'l.post_id = %d', $args['source_id'] ); } if ( ! empty( $args['target_post_id'] ) ) { $where[] = $wpdb->prepare( 'l.target_post_id = %d', $args['target_post_id'] ); } if ( isset( $args['is_internal'] ) && '' !== $args['is_internal'] ) { $type = '1' === $args['is_internal'] ? 'internal' : 'external'; $where[] = $wpdb->prepare( 'l.type = %s', $type ); } if ( ! empty( $args['search'] ) ) { $where[] = $wpdb->prepare( 'l.url LIKE %s', '%' . $wpdb->esc_like( $args['search'] ) . '%' ); } return implode( ' AND ', $where ); } /** * Validate ORDER direction. * * @param string $order Input order. * @param string $default_order Default order. * @return string */ private function validate_order( $order, $default_order = 'DESC' ) { $order = strtoupper( $order ); return in_array( $order, [ 'ASC', 'DESC' ], true ) ? $order : $default_order; } /** * Validate and return ORDER BY column for posts query. * * @param string $orderby Input orderby. * @return string */ private function validate_posts_orderby( $orderby ) { $map = [ 'post_title' => 'p.post_title', 'internal_link_count' => 'm.internal_link_count', 'external_link_count' => 'm.external_link_count', 'incoming_link_count' => 'm.incoming_link_count', 'seo_score' => 'pm_score.meta_value', 'post_modified' => 'p.post_modified', ]; return isset( $map[ $orderby ] ) ? $map[ $orderby ] : 'p.post_title'; } /** * Validate and return ORDER BY column for links query. * * @param string $orderby Input orderby. * @return string */ private function validate_links_orderby( $orderby ) { $map = [ 'id' => 'l.id', 'url' => 'l.url', 'type' => 'l.type', 'source_title' => 'sp.post_title', ]; return isset( $map[ $orderby ] ) ? $map[ $orderby ] : 'l.id'; } /** * Get relative permalink (without home_url prefix). * * @param int $post_id Post ID. * @return string */ private function get_relative_permalink( $post_id ) { if ( empty( $post_id ) ) { return ''; } $permalink = get_permalink( $post_id ); if ( ! $permalink ) { return ''; } return str_replace( untrailingslashit( home_url() ), '', $permalink ); } /** * Get argument definitions for the /posts endpoint. * * @return array */ private function get_posts_args() { return [ 'page' => [ 'description' => esc_html__( 'Page number.', 'seo-by-rank-math' ), 'type' => 'integer', 'default' => 1, ], 'per_page' => [ 'description' => esc_html__( 'Items per page.', 'seo-by-rank-math' ), 'type' => 'integer', 'default' => 50, 'maximum' => 100, ], 'search' => [ 'description' => esc_html__( 'Search in post title.', 'seo-by-rank-math' ), 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ], 'post_type' => [ 'description' => esc_html__( 'Filter by post type. Supports multiple values.', 'seo-by-rank-math' ), 'type' => 'array', 'items' => [ 'type' => 'string' ], 'default' => [], 'sanitize_callback' => function ( $param ) { return array_map( 'sanitize_text_field', (array) $param ); }, ], 'is_orphan' => [ 'description' => esc_html__( 'Filter by orphan status (orphan, linked).', 'seo-by-rank-math' ), 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ], 'seo_score_range' => [ 'description' => esc_html__( 'Filter by SEO score range (great, good, bad, no-score).', 'seo-by-rank-math' ), 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ], 'post_id' => [ 'description' => esc_html__( 'Filter by post ID.', 'seo-by-rank-math' ), 'type' => 'integer', 'default' => 0, 'sanitize_callback' => 'absint', ], 'orderby' => [ 'description' => esc_html__( 'Order by field.', 'seo-by-rank-math' ), 'type' => 'string', 'default' => 'post_title', 'sanitize_callback' => 'sanitize_text_field', ], 'order' => [ 'description' => esc_html__( 'Order direction (ASC, DESC).', 'seo-by-rank-math' ), 'type' => 'string', 'default' => 'ASC', 'sanitize_callback' => 'sanitize_text_field', ], ]; } /** * Get argument definitions for the /links endpoint. * * @return array */ private function get_links_args() { return [ 'page' => [ 'description' => esc_html__( 'Page number.', 'seo-by-rank-math' ), 'type' => 'integer', 'default' => 1, ], 'per_page' => [ 'description' => esc_html__( 'Items per page.', 'seo-by-rank-math' ), 'type' => 'integer', 'default' => 50, 'maximum' => 100, ], 'search' => [ 'description' => esc_html__( 'Search in URL.', 'seo-by-rank-math' ), 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ], 'source_id' => [ 'description' => esc_html__( 'Filter by source post ID.', 'seo-by-rank-math' ), 'type' => 'integer', 'default' => 0, ], 'target_post_id' => [ 'description' => esc_html__( 'Filter by target post ID.', 'seo-by-rank-math' ), 'type' => 'integer', 'default' => 0, ], 'is_internal' => [ 'description' => esc_html__( 'Filter by link type (1 = internal, 0 = external).', 'seo-by-rank-math' ), 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ], 'orderby' => [ 'description' => esc_html__( 'Order by field.', 'seo-by-rank-math' ), 'type' => 'string', 'default' => 'id', 'sanitize_callback' => 'sanitize_text_field', ], 'order' => [ 'description' => esc_html__( 'Order direction (ASC, DESC).', 'seo-by-rank-math' ), 'type' => 'string', 'default' => 'DESC', 'sanitize_callback' => 'sanitize_text_field', ], ]; } }
💾 Save Changes
❌ Cancel