settings = $settings; $this->types = $types; $this->meta = $meta; $this->templates = $templates; } /** * Clears the per-request cache (used by tests and after query changes). */ public function reset() { $this->current = false; } /** * Values for the main query, or null for views Orbitstem does not manage. * * @return array|null */ public function for_request() { if ( false === $this->current ) { $data = $this->resolve_request(); /** * Filters the resolved SEO values for the current request. * * @param array|null $data Resolved values, or null when unmanaged. */ $this->current = apply_filters( 'orbitstem_seo_resolved', $data ); } return $this->current; } /** * Dispatches on the WordPress conditional tags. * * @return array|null */ private function resolve_request() { $paged = max( 1, (int) get_query_var( 'paged' ) ); if ( is_front_page() ) { return $this->for_front_page( max( $paged, (int) get_query_var( 'page' ) ) ); } if ( is_home() ) { $posts_page = (int) get_option( 'page_for_posts' ); $post = $posts_page ? get_post( $posts_page ) : null; return $post ? $this->for_post( $post, $paged, true ) : null; } if ( is_singular() ) { $post = get_queried_object(); return $post instanceof WP_Post ? $this->for_post( $post ) : null; } if ( is_category() || is_tag() || is_tax() ) { $term = get_queried_object(); return $term instanceof WP_Term ? $this->for_term( $term, $paged ) : null; } if ( is_post_type_archive() ) { $type = get_query_var( 'post_type' ); $type = is_array( $type ) ? reset( $type ) : $type; return $this->for_post_type_archive( (string) $type, $paged ); } if ( is_author() ) { return $this->for_author( (int) get_queried_object_id(), $paged ); } if ( is_date() ) { return $this->for_date_archive( $paged ); } if ( is_search() ) { $data = $this->simple( $this->settings->get( 'search_title' ), array( 'search' => TemplateEngine::plain( get_search_query( false ) ), 'page' => $this->page_label( $paged ), ), true ); $data['object_type'] = 'search'; $data['url'] = get_search_link(); return $data; } if ( is_404() ) { $data = $this->simple( $this->settings->get( 'not_found_title' ), array( 'not_found' => __( 'Page not found', 'orbitstem-seo' ) ), true ); $data['object_type'] = '404'; return $data; } return null; } /** * Values for a post of a managed type. * * @param WP_Post $post Post. * @param int $paged Archive page (posts page only). * @param bool $posts_page Whether the post is the "Posts page" listing. * @return array|null */ public function for_post( WP_Post $post, $paged = 1, $posts_page = false ) { if ( ! $this->types->supports_post_type( $post->post_type ) ) { return null; } $type_object = get_post_type_object( $post->post_type ); if ( ! $posts_page && is_singular() && (int) get_queried_object_id() === (int) $post->ID ) { $paged = max( 1, (int) get_query_var( 'page' ) ); // Split posts (): page 2 and later. } $vars = array( 'title' => TemplateEngine::plain( $post->post_title ), 'excerpt' => $this->post_excerpt( $post ), 'type' => $type_object ? TemplateEngine::plain( $type_object->labels->name ) : '', 'page' => $this->page_label( $paged ), ); $custom_title = $this->meta->get_post( $post->ID, 'title' ); $custom_desc = $this->meta->get_post( $post->ID, 'description' ); $custom_canon = $this->meta->get_post( $post->ID, 'canonical' ); $custom_schema = $this->meta->get_post( $post->ID, 'schema' ); if ( $posts_page ) { $canonical = $this->paged_url( get_permalink( $post ), $paged ); $own_url = $canonical; } else { $canonical = wp_get_canonical_url( $post ); $own_url = $canonical ? $canonical : get_permalink( $post ); } $image = get_the_post_thumbnail_url( $post, 'large' ); $social_image = $this->meta->get_post( $post->ID, 'social_image' ); $social_title = $this->meta->get_post( $post->ID, 'social_title' ); $social_desc = $this->meta->get_post( $post->ID, 'social_description' ); return array( 'title' => $this->templates->render( '' !== $custom_title ? $custom_title : $this->settings->post_type( $post->post_type, 'title' ), $vars ), 'description' => $this->templates->render( '' !== $custom_desc ? $custom_desc : $this->settings->post_type( $post->post_type, 'description' ), $vars ), // Share previews: their own text when set, otherwise the SEO title and description. 'og_title' => '' !== $social_title ? $this->templates->render( $social_title, $vars ) : '', 'og_desc' => '' !== $social_desc ? $this->templates->render( $social_desc, $vars ) : '', 'canonical' => '' !== $custom_canon ? $custom_canon : (string) $canonical, // The posts page lists posts; it does not inherit the "Pages" noindex setting. 'noindex' => $this->meta->get_post( $post->ID, 'noindex' ) || ( ! $posts_page && (bool) $this->settings->post_type( $post->post_type, 'noindex' ) ), 'nofollow' => (bool) $this->meta->get_post( $post->ID, 'nofollow' ), 'og_type' => 'post' === $post->post_type && ! $posts_page ? 'article' : 'website', 'image' => '' !== $social_image ? $social_image : ( $image ? $image : (string) $this->settings->get( 'og_default_image' ) ), 'published' => 'post' === $post->post_type ? get_post_time( 'c', true, $post ) : '', 'modified' => 'post' === $post->post_type ? get_post_modified_time( 'c', true, $post ) : '', 'object_type' => 'post', 'object_id' => $post->ID, 'url' => (string) $own_url, 'schema' => $posts_page ? 'WebPage' : ( '' !== $custom_schema ? $custom_schema : (string) $this->settings->post_type( $post->post_type, 'schema' ) ), ); } /** * Only the indexing signals of a post: noindex, canonical and its own URL. * Much cheaper than for_post() (no excerpt, templates or images), for scans. * * @param WP_Post $post Post. * @return array{noindex:bool,canonical:string,url:string}|null Null for unmanaged types. */ public function indexing_for_post( WP_Post $post ) { if ( ! $this->types->supports_post_type( $post->post_type ) ) { return null; } $own = (string) get_permalink( $post ); $custom = $this->meta->get_post( $post->ID, 'canonical' ); $special = 'page' === get_option( 'show_on_front' ) && in_array( (int) $post->ID, array( (int) get_option( 'page_on_front' ), (int) get_option( 'page_for_posts' ) ), true ); return array( 'noindex' => $this->meta->get_post( $post->ID, 'noindex' ) || ( ! $special && (bool) $this->settings->post_type( $post->post_type, 'noindex' ) ), 'canonical' => '' !== $custom ? $custom : $own, 'url' => $own, ); } /** * Values for a term of a managed taxonomy. * * @param WP_Term $term Term. * @param int $paged Archive page. * @return array|null */ public function for_term( WP_Term $term, $paged = 1 ) { if ( ! $this->types->supports_taxonomy( $term->taxonomy ) ) { return null; } $vars = array( 'term' => TemplateEngine::plain( $term->name ), 'term_description' => TemplateEngine::truncate( strip_shortcodes( $term->description ) ), 'page' => $this->page_label( $paged ), ); $custom_title = $this->meta->get_term( $term->term_id, 'title' ); $custom_desc = $this->meta->get_term( $term->term_id, 'description' ); $custom_canon = $this->meta->get_term( $term->term_id, 'canonical' ); $link = get_term_link( $term ); $own_url = is_wp_error( $link ) ? '' : $this->paged_url( $link, $paged ); return array( 'title' => $this->templates->render( '' !== $custom_title ? $custom_title : $this->settings->taxonomy( $term->taxonomy, 'title' ), $vars ), 'description' => $this->templates->render( '' !== $custom_desc ? $custom_desc : $this->settings->taxonomy( $term->taxonomy, 'description' ), $vars ), 'canonical' => '' !== $custom_canon ? $custom_canon : $own_url, // An empty archive ("thin page") is noindex when Settings says so. 'noindex' => $this->meta->get_term( $term->term_id, 'noindex' ) || (bool) $this->settings->taxonomy( $term->taxonomy, 'noindex' ) || ( 0 === (int) $term->count && $this->settings->get( 'noindex_empty_terms' ) ), 'nofollow' => (bool) $this->meta->get_term( $term->term_id, 'nofollow' ), 'og_type' => 'website', 'image' => (string) $this->settings->get( 'og_default_image' ), 'published' => '', 'modified' => '', 'object_type' => 'term', 'object_id' => $term->term_id, 'url' => $own_url, 'schema' => 'CollectionPage', ); } /** * Values for the front page (static page or latest posts). * * @param int $paged Page number. * @return array */ private function for_front_page( $paged ) { $paged = max( 1, (int) $paged ); $vars = array( 'page' => $this->page_label( $paged ) ); $title = $this->templates->render( $this->settings->get( 'home_title' ), $vars ); $desc = $this->templates->render( $this->settings->get( 'home_description' ), $vars ); $data = null; $front_id = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_on_front' ) : 0; $front = $front_id ? get_post( $front_id ) : null; if ( $front instanceof WP_Post ) { $data = $this->for_post( $front ); } if ( null === $data ) { $data = $this->simple( '', array(), false ); } // The page's own fields win; otherwise the homepage templates apply. if ( ! $front || '' === $this->meta->get_post( $front->ID, 'title' ) ) { $data['title'] = $title; } // An empty homepage template (e.g. no tagline) falls back to the front page's own text. if ( ( ! $front || '' === $this->meta->get_post( $front->ID, 'description' ) ) && '' !== $desc ) { $data['description'] = $desc; } $home_url = $this->paged_url( home_url( '/' ), $paged ); if ( ! $front || '' === $this->meta->get_post( $front->ID, 'canonical' ) ) { $data['canonical'] = $home_url; } // The homepage never inherits the "Pages" noindex setting; only its own field counts. $data['noindex'] = $front ? $this->meta->get_post( $front->ID, 'noindex' ) : false; $data['og_type'] = 'website'; $data['published'] = ''; $data['modified'] = ''; $data['object_type'] = 'front'; $data['object_id'] = $front ? $front->ID : 0; $data['url'] = $home_url; $data['schema'] = $front ? 'WebPage' : 'CollectionPage'; return $data; } /** * Values for a post type archive, e.g. /products/. * * @param string $post_type Post type name. * @param int $paged Page number. * @return array|null */ private function for_post_type_archive( $post_type, $paged ) { if ( ! $this->types->supports_post_type( $post_type ) ) { return null; } $object = get_post_type_object( $post_type ); $vars = array( 'type' => $object ? TemplateEngine::plain( $object->labels->name ) : '', 'page' => $this->page_label( $paged ), ); $link = get_post_type_archive_link( $post_type ); $data = $this->simple( $this->settings->post_type( $post_type, 'archive_title' ), $vars, false ); $data['description'] = $this->templates->render( $this->settings->post_type( $post_type, 'archive_description' ), $vars ); $data['canonical'] = $link ? $this->paged_url( $link, $paged ) : ''; $data['object_type'] = 'archive'; $data['url'] = $data['canonical']; $data['schema'] = 'CollectionPage'; return $data; } /** * Values for an author archive. * * @param int $user_id Author ID. * @param int $paged Page number. * @return array */ private function for_author( $user_id, $paged ) { $group = (array) $this->settings->get( 'author_archive' ); $vars = array( 'author' => TemplateEngine::plain( get_the_author_meta( 'display_name', $user_id ) ), 'author_description' => TemplateEngine::truncate( strip_shortcodes( get_the_author_meta( 'description', $user_id ) ) ), 'page' => $this->page_label( $paged ), ); $data = $this->simple( isset( $group['title'] ) ? $group['title'] : '', $vars, ! empty( $group['noindex'] ) ); $data['description'] = $this->templates->render( isset( $group['description'] ) ? $group['description'] : '', $vars ); $data['url'] = $this->paged_url( get_author_posts_url( $user_id ), $paged ); $data['canonical'] = $data['noindex'] ? '' : $data['url']; $data['object_type'] = 'author'; $data['object_id'] = $user_id; $data['schema'] = 'ProfilePage'; return $data; } /** * Values for a year, month or day archive. * * @param int $paged Page number. * @return array */ private function for_date_archive( $paged ) { $group = (array) $this->settings->get( 'date_archive' ); $year = (int) get_query_var( 'year' ); $month = (int) get_query_var( 'monthnum' ); $day = (int) get_query_var( 'day' ); if ( is_day() ) { $label = date_i18n( get_option( 'date_format' ), mktime( 0, 0, 0, $month, $day, $year ) ); $link = get_day_link( $year, $month, $day ); } elseif ( is_month() ) { $label = date_i18n( 'F Y', mktime( 0, 0, 0, $month, 1, $year ) ); $link = get_month_link( $year, $month ); } else { $label = (string) $year; $link = get_year_link( $year ); } $vars = array( 'date' => $label, 'page' => $this->page_label( $paged ), ); $data = $this->simple( isset( $group['title'] ) ? $group['title'] : '', $vars, ! empty( $group['noindex'] ) ); $data['description'] = $this->templates->render( isset( $group['description'] ) ? $group['description'] : '', $vars ); $data['url'] = $this->paged_url( $link, $paged ); $data['canonical'] = $data['noindex'] ? '' : $data['url']; $data['object_type'] = 'date'; $data['schema'] = 'CollectionPage'; return $data; } /** * A result with only a title and robots value. * * @param string $template Title template. * @param array $vars Template values. * @param bool $noindex Whether to noindex. * @return array */ private function simple( $template, array $vars, $noindex ) { return array( 'title' => '' === (string) $template ? '' : $this->templates->render( $template, $vars ), 'description' => '', 'canonical' => '', 'noindex' => (bool) $noindex, 'nofollow' => false, 'og_type' => 'website', 'image' => (string) $this->settings->get( 'og_default_image' ), 'published' => '', 'modified' => '', 'object_type' => '', 'object_id' => 0, 'url' => '', 'schema' => 'WebPage', ); } /** * Generated description for a post: excerpt, else the start of the content. * * @param WP_Post $post Post. * @return string */ private function post_excerpt( WP_Post $post ) { if ( '' !== $post->post_password ) { return ''; } $text = '' !== trim( $post->post_excerpt ) ? $post->post_excerpt : $post->post_content; $text = strip_shortcodes( excerpt_remove_blocks( $text ) ); return TemplateEngine::truncate( $text ); } /** * "Page 2" label for paginated views, empty on page 1. * * @param int $paged Page number. * @return string */ private function page_label( $paged ) { /* translators: %d: page number */ return $paged > 1 ? sprintf( __( 'Page %d', 'orbitstem-seo' ), $paged ) : ''; } /** * Adds pagination to an archive URL. * * @param string $base Page-1 URL. * @param int $paged Page number. * @return string */ private function paged_url( $base, $paged ) { global $wp_rewrite; $base = (string) $base; if ( '' === $base || $paged < 2 ) { return $base; } if ( $wp_rewrite->using_permalinks() && false === strpos( $base, '?' ) ) { return user_trailingslashit( trailingslashit( $base ) . $wp_rewrite->pagination_base . '/' . $paged, 'paged' ); } return add_query_arg( 'paged', $paged, $base ); } }