plugin = $plugin; } /** * Capability needed to view the dashboard and change settings. * * @return string */ public static function capability() { /** * Filters the capability for Orbitstem SEO admin screens. * * @param string $capability Capability name. */ return (string) apply_filters( 'orbitstem_seo_manage_capability', 'manage_options' ); } /** * Registers hooks. */ public function register() { add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_init', array( $this, 'register_setting' ) ); add_filter( 'option_page_capability_orbitstem_seo', array( __CLASS__, 'capability' ) ); add_filter( 'plugin_action_links_' . plugin_basename( ORBITSTEM_SEO_FILE ), array( $this, 'action_links' ) ); add_action( 'admin_post_orbitstem_seo_review', array( $this, 'review_answer' ) ); add_action( 'admin_notices', array( $this, 'staging_notice' ) ); } /** * What staging protection is really doing: 'on' (pages ask not to be * indexed), 'paused' (it would apply, but safe mode keeps Orbitstem from * changing robots tags and robots.txt while another SEO plugin is active), * or '' (it does not apply here). * * @param Plugin $plugin Plugin services. * @return string */ public static function staging_state( Plugin $plugin ) { if ( ! \Orbitstem\SEO\Frontend\Robots::guard_applies( $plugin->settings ) ) { return ''; } // Robots, which adds the noindex, is only registered when output is on (see Plugin::setup()). return $plugin->output_enabled() ? 'on' : 'paused'; } /** * What staging protection is doing and why, in one sentence. * * @param string $state 'on' or 'paused', from staging_state(). * @return string */ public static function staging_warning( $state = 'on' ) { if ( 'paused' === $state ) { return sprintf( /* translators: %s: reason, such as 'WordPress is set up as a "staging" site.' */ __( 'Staging protection is paused: another SEO plugin is active, so Orbitstem SEO is in safe mode and does not ask search engines to stay out of this copy. Why this looks like a copy: %s To keep it out of search results, use the other SEO plugin\'s settings or "Discourage search engines" in Settings > Reading.', 'orbitstem-seo' ), \Orbitstem\SEO\Frontend\Robots::staging_reason() ); } return sprintf( /* translators: %s: reason, such as 'WordPress is set up as a "staging" site.' */ __( 'Staging protection is ON: every page of this site tells search engines not to index it, and robots.txt asks all crawlers to stay out. Reason: %s If this is your live site, turn off "Staging protection" in Orbitstem SEO > Settings > Crawlers now.', 'orbitstem-seo' ), \Orbitstem\SEO\Frontend\Robots::staging_reason() ); } /** * Prints the staging protection warning as a notice ('' state: nothing). * * @param string $state 'on' or 'paused', from staging_state(). * @param bool $inline Inline notice inside an Orbitstem screen (WordPress leaves it where it is). */ public static function print_staging_warning( $state, $inline = false ) { if ( '' === $state ) { return; } printf( '

%2$s %3$s %5$s

', $inline ? ' inline' : '', esc_html__( 'Orbitstem SEO:', 'orbitstem-seo' ), esc_html( self::staging_warning( $state ) ), esc_url( admin_url( 'admin.php?page=' . self::SETTINGS_SLUG . '#os-tab-crawlers' ) ), esc_html__( 'Open settings', 'orbitstem-seo' ) ); } /** * Shows administrators on every admin screen that the site is hidden from * search engines by staging protection, so it is never left on by mistake. */ public function staging_notice() { if ( ! current_user_can( self::capability() ) ) { return; } // The plugin's own pages show it at the top themselves (Ui::open()). if ( Ui::is_screen() ) { return; } $state = self::staging_state( $this->plugin ); // "Paused" is only news on the plugin's own pages; elsewhere it would be noise. if ( 'on' === $state ) { self::print_staging_warning( $state ); } } /** * Adds the menu and submenus. */ public function menu() { add_menu_page( __( 'Orbitstem SEO', 'orbitstem-seo' ), __( 'Orbitstem SEO', 'orbitstem-seo' ), self::capability(), self::SLUG, array( $this, 'render_dashboard' ), 'dashicons-search', 81 ); add_submenu_page( self::SLUG, __( 'Dashboard', 'orbitstem-seo' ), __( 'Dashboard', 'orbitstem-seo' ), self::capability(), self::SLUG, array( $this, 'render_dashboard' ) ); add_submenu_page( self::SLUG, __( 'Settings', 'orbitstem-seo' ), __( 'Settings', 'orbitstem-seo' ), self::capability(), self::SETTINGS_SLUG, array( $this, 'render_settings' ) ); } /** * Registers the settings option with its sanitizer. */ public function register_setting() { register_setting( 'orbitstem_seo', Settings::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( Settings::class, 'sanitize' ), 'show_in_rest' => false, ) ); } /** * Adds a Settings link on the Plugins screen. * * @param string[] $links Links. * @return string[] */ public function action_links( $links ) { array_unshift( $links, sprintf( '%s', esc_url( admin_url( 'admin.php?page=' . self::SETTINGS_SLUG ) ), esc_html__( 'Settings', 'orbitstem-seo' ) ) ); return $links; } /** * Health problems worth showing first, as [ severity, message ] pairs. * Messages are plain text; the caller escapes them. * * @return array */ public function status_messages() { $messages = array(); $staging = \Orbitstem\SEO\Frontend\Robots::looks_like_staging(); if ( '0' === (string) get_option( 'blog_public' ) ) { $messages[] = array( 'error', $staging ? __( 'Search engines are discouraged from indexing this site (Settings > Reading). Every page is sent with noindex.', 'orbitstem-seo' ) : __( 'This looks like a live site, but "Discourage search engines" is on (Settings > Reading), so every page is hidden from search. This is a common mistake after going live: switch it off unless you really mean it.', 'orbitstem-seo' ), ); } $staging_state = self::staging_state( $this->plugin ); if ( '' !== $staging_state ) { // "On" hides the whole site from search, so it is an error; "paused" leaves the copy indexable. $messages[] = array( 'on' === $staging_state ? 'error' : 'warning', self::staging_warning( $staging_state ) ); } elseif ( ! $staging && '' !== \Orbitstem\SEO\Frontend\Robots::staging_like_address() ) { $messages[] = array( 'info', sprintf( /* translators: %s: host name */ __( 'The address %s looks like a staging copy, but WordPress is set up as a live site, so staging protection does nothing here. If this is a copy, add define( \'WP_ENVIRONMENT_TYPE\', \'staging\' ); to wp-config.php and search engines will be asked to stay out.', 'orbitstem-seo' ), \Orbitstem\SEO\Frontend\Robots::staging_like_address() ), ); } $conflicts = $this->plugin->detector->detect(); foreach ( $conflicts as $found ) { $messages[] = array( 'warning', sprintf( /* translators: 1: plugin name, 2: evidence */ __( 'Another SEO plugin is active: %1$s (%2$s)', 'orbitstem-seo' ), $found['name'], $found['evidence'] ), ); } if ( $conflicts && ! $this->plugin->output_enabled() ) { $messages[] = array( 'warning', __( 'Safe mode: Orbitstem SEO prints no head tags while another SEO plugin is active, to avoid duplicates. You can turn output on under Settings > Advanced.', 'orbitstem-seo' ) ); } elseif ( $conflicts ) { $messages[] = array( 'error', __( 'Output is forced on while another SEO plugin is active. Pages may contain duplicate titles, descriptions or canonical tags.', 'orbitstem-seo' ) ); } // Block themes print the title through core's document-title functions without declaring title-tag. if ( ! current_theme_supports( 'title-tag' ) && ! wp_is_block_theme() ) { $messages[] = array( 'warning', __( 'The active theme does not declare title-tag support, so Orbitstem SEO cannot set the page title.', 'orbitstem-seo' ) ); } $new_types = array_merge( $this->plugin->types->unconfigured_post_types(), $this->plugin->types->unconfigured_taxonomies() ); if ( $new_types ) { $messages[] = array( 'info', sprintf( /* translators: %s: comma-separated machine names */ __( 'Using default templates for: %s. Review them in Settings.', 'orbitstem-seo' ), implode( ', ', $new_types ) ), ); } return $messages; } /** * Per-type counts of published items and items with custom fields. * * @return array */ public function coverage() { $cached = get_transient( self::COVERAGE_TRANSIENT ); if ( is_array( $cached ) ) { return $cached; } $rows = $this->compute_coverage(); set_transient( self::COVERAGE_TRANSIENT, $rows, 10 * MINUTE_IN_SECONDS ); return $rows; } /** * Uncached coverage counts (two COUNT queries per content type). * * @return array */ private function compute_coverage() { global $wpdb; $rows = array(); foreach ( $this->plugin->types->post_types() as $name => $object ) { $counts = wp_count_posts( $name ); $row = array( 'label' => $object->labels->name, 'name' => $name, 'kind' => 'post_type', 'total' => isset( $counts->publish ) ? (int) $counts->publish : 0, 'noindex' => (bool) $this->plugin->settings->post_type( $name, 'noindex' ), 'title' => 0, 'description' => 0, ); foreach ( array( 'title', 'description' ) as $field ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- aggregate count, no core API. $row[ $field ] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} m ON m.post_id = p.ID AND m.meta_key = %s AND m.meta_value <> '' WHERE p.post_type = %s AND p.post_status = 'publish'", MetaRepository::PREFIX . $field, $name ) ); } $rows[] = $row; } foreach ( $this->plugin->types->taxonomies() as $name => $object ) { $total = wp_count_terms( array( 'taxonomy' => $name, 'hide_empty' => false, ) ); $row = array( 'label' => $object->labels->name, 'name' => $name, 'kind' => 'taxonomy', 'total' => is_wp_error( $total ) ? 0 : (int) $total, 'noindex' => (bool) $this->plugin->settings->taxonomy( $name, 'noindex' ), 'title' => 0, 'description' => 0, ); foreach ( array( 'title', 'description' ) as $field ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- aggregate count, no core API. $row[ $field ] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT tm.term_id) FROM {$wpdb->termmeta} tm INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = tm.term_id WHERE tt.taxonomy = %s AND tm.meta_key = %s AND tm.meta_value <> ''", $name, MetaRepository::PREFIX . $field ) ); } $rows[] = $row; } return $rows; } /** * Dashboard screen: one screen, no scrolling needed for the essentials. * Status line, six tiles, next steps and coverage side by side, tools. */ public function render_dashboard() { if ( ! current_user_can( self::capability() ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'orbitstem-seo' ) ); } $messages = $this->status_messages(); $summary = $this->plugin->issues->summary(); $issues = admin_url( 'admin.php?page=' . IssuesPage::SLUG ); $alert = $this->plugin->monitor->alert(); $autopilot = $this->plugin->autopilot->enabled_fixers(); $last_run = $this->plugin->autopilot->last(); $scan_form = sprintf( '
%3$s
', esc_url( admin_url( 'admin-post.php' ) ), esc_attr( IssuesPage::ACTION ), wp_nonce_field( IssuesPage::ACTION, '_wpnonce', true, false ), esc_html__( 'Run scan', 'orbitstem-seo' ) ); $actions = $scan_form . sprintf( '%2$s', esc_url( admin_url( 'admin.php?page=' . self::SETTINGS_SLUG ) ), esc_html__( 'Settings', 'orbitstem-seo' ) ); Ui::open( self::SLUG, __( 'Dashboard', 'orbitstem-seo' ), __( 'What needs attention on this site, and where to fix it.', 'orbitstem-seo' ), $actions ); RedirectsPage::print_notice(); $this->review_request(); ?>

plugin->output_enabled() && ! array_filter( $messages, array( $this, 'is_problem' ) ) ) : ?>
'dashicons-dismiss', 'warning' => 'dashicons-warning', 'info' => 'dashicons-info', ); foreach ( $messages as $message ) : ?>
0, 'warning' => 0, 'notice' => 0, ); foreach ( array( 'error' => __( 'Errors', 'orbitstem-seo' ), 'warning' => __( 'Warnings', 'orbitstem-seo' ), 'notice' => __( 'Notices', 'orbitstem-seo' ), ) as $severity => $label ) { Ui::tile( number_format_i18n( $summary[ $severity ] ), $label, $summary[ $severity ] || 'notice' === $severity ? $severity : 'success', $issues . '#filter-' . $severity, /* translators: %d: number of issue types */ $summary[ $severity ] ? sprintf( _n( 'Items in %d issue type', 'Items in %d issue types', $groups[ $severity ], 'orbitstem-seo' ), $groups[ $severity ] ) : __( 'None found', 'orbitstem-seo' ) ); } } else { Ui::tile( '–', __( 'No scan yet', 'orbitstem-seo' ), 'brand', $issues, __( 'Run a scan to find issues', 'orbitstem-seo' ) ); } $missing = $this->plugin->not_found->count(); /* translators: %s: number of addresses */ Ui::tile( number_format_i18n( $this->plugin->redirects->count() ), __( 'Redirects', 'orbitstem-seo' ), 'neutral', admin_url( 'admin.php?page=' . RedirectsPage::SLUG ), sprintf( _n( '%s address in the 404 log', '%s addresses in the 404 log', $missing, 'orbitstem-seo' ), number_format_i18n( $missing ) ) ); $unexplained = $alert ? (int) $alert['counts']['unexplained']['error'] + (int) $alert['counts']['unexplained']['warning'] : 0; Ui::tile( $alert ? number_format_i18n( $unexplained ) : __( 'OK', 'orbitstem-seo' ), __( 'Monitor', 'orbitstem-seo' ), $alert ? 'error' : 'success', admin_url( 'admin.php?page=' . MonitorPage::SLUG ), $alert ? __( 'Unexplained SEO changes', 'orbitstem-seo' ) : __( 'No unexplained changes', 'orbitstem-seo' ) ); Ui::tile( $autopilot ? __( 'On', 'orbitstem-seo' ) : __( 'Off', 'orbitstem-seo' ), __( 'Automatic repairs', 'orbitstem-seo' ), $autopilot ? 'success' : 'neutral', admin_url( 'admin.php?page=' . RepairPage::SLUG ), /* translators: %d: number of repairs */ $last_run ? sprintf( _n( 'Last run: %d repair', 'Last run: %d repairs', (int) $last_run['applied'], 'orbitstem-seo' ), (int) $last_run['applied'] ) : __( 'Safe link repairs after each scan', 'orbitstem-seo' ) ); ?>
__( 'Error', 'orbitstem-seo' ), 'warning' => __( 'Warning', 'orbitstem-seo' ), 'notice' => __( 'Notice', 'orbitstem-seo' ), ); ?>

coverage(); $shown = array_filter( $rows, function ( $row ) { return $row['total'] > 0; } ); ?>
count( $shown ) ) : ?>

plugin->settings; $opt = Settings::OPTION; $tabs = array( 'os-tab-general' => array( __( 'General', 'orbitstem-seo' ), 'dashicons-admin-site-alt3' ), 'os-tab-content' => array( __( 'Content types', 'orbitstem-seo' ), 'dashicons-admin-post' ), 'os-tab-taxonomies' => array( __( 'Taxonomies', 'orbitstem-seo' ), 'dashicons-tag' ), 'os-tab-archives' => array( __( 'Archives & search', 'orbitstem-seo' ), 'dashicons-archive' ), 'os-tab-structure' => array( __( 'Sitemap & schema', 'orbitstem-seo' ), 'dashicons-networking' ), 'os-tab-local' => array( __( 'Local SEO', 'orbitstem-seo' ), 'dashicons-location' ), 'os-tab-crawlers' => array( __( 'Crawlers', 'orbitstem-seo' ), 'dashicons-shield' ), 'os-tab-checks' => array( __( 'Checks & history', 'orbitstem-seo' ), 'dashicons-visibility' ), 'os-tab-automation' => array( __( 'Automation', 'orbitstem-seo' ), 'dashicons-controls-repeat' ), 'os-tab-advanced' => array( __( 'Advanced', 'orbitstem-seo' ), 'dashicons-admin-tools' ), ); Ui::open( self::SETTINGS_SLUG, __( 'Settings', 'orbitstem-seo' ), __( 'Templates and switches for every content type. Each tab is saved together with the others.', 'orbitstem-seo' ) ); ?>

text_row( __( 'Homepage title', 'orbitstem-seo' ), $opt . '[home_title]', $s->get( 'home_title' ) ); $this->text_row( __( 'Homepage description', 'orbitstem-seo' ), $opt . '[home_description]', $s->get( 'home_description' ) ); $this->text_row( __( 'Default social image URL', 'orbitstem-seo' ), $opt . '[og_default_image]', $s->get( 'og_default_image' ), __( 'Used for og:image when a page has no featured image.', 'orbitstem-seo' ), 'url' ); $this->text_row( __( 'X (Twitter) username', 'orbitstem-seo' ), $opt . '[twitter_site]', $s->get( 'twitter_site' ), __( 'Without the @.', 'orbitstem-seo' ) ); ?>

$help ) : ?>

plugin->types->post_types() as $name => $object ) : ?>
> labels->name ); ?> post_type( $name, 'noindex' ) ? Ui::badge( 'warning', __( 'noindex', 'orbitstem-seo' ) ) : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in Ui::badge(). ?>
text_row( __( 'Title template', 'orbitstem-seo' ), $base . '[title]', $s->post_type( $name, 'title' ) ); $this->text_row( __( 'Description template', 'orbitstem-seo' ), $base . '[description]', $s->post_type( $name, 'description' ) ); $this->checkbox_row( __( 'Search engines', 'orbitstem-seo' ), $base . '[noindex]', $s->post_type( $name, 'noindex' ), __( 'Hide this type from search results (noindex)', 'orbitstem-seo' ) ); $this->checkbox_row( __( 'XML sitemap', 'orbitstem-seo' ), $base . '[sitemap]', $s->post_type( $name, 'sitemap' ), __( 'List this type in the sitemap (never when noindex)', 'orbitstem-seo' ) ); $this->select_row( __( 'Schema type', 'orbitstem-seo' ), $base . '[schema]', $s->post_type( $name, 'schema' ), array_combine( Settings::schema_types(), Settings::schema_types() ) ); if ( $object->has_archive ) { $this->text_row( __( 'Archive title template', 'orbitstem-seo' ), $base . '[archive_title]', $s->post_type( $name, 'archive_title' ) ); $this->text_row( __( 'Archive description', 'orbitstem-seo' ), $base . '[archive_description]', $s->post_type( $name, 'archive_description' ) ); } else { // esc_textarea() encodes every "&", so these unchanged values are sent back exactly. printf( '', esc_attr( $base . '[archive_title]' ), esc_textarea( $s->post_type( $name, 'archive_title' ) ), esc_attr( $base . '[archive_description]' ), esc_textarea( $s->post_type( $name, 'archive_description' ) ) ); } ?>

plugin->types->taxonomies() as $name => $object ) : ?>
> labels->name ); ?> taxonomy( $name, 'noindex' ) ? Ui::badge( 'warning', __( 'noindex', 'orbitstem-seo' ) ) : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in Ui::badge(). ?>
text_row( __( 'Title template', 'orbitstem-seo' ), $base . '[title]', $s->taxonomy( $name, 'title' ) ); $this->text_row( __( 'Description template', 'orbitstem-seo' ), $base . '[description]', $s->taxonomy( $name, 'description' ) ); $this->checkbox_row( __( 'Search engines', 'orbitstem-seo' ), $base . '[noindex]', $s->taxonomy( $name, 'noindex' ), __( 'Hide these archives from search results (noindex)', 'orbitstem-seo' ) ); $this->checkbox_row( __( 'XML sitemap', 'orbitstem-seo' ), $base . '[sitemap]', $s->taxonomy( $name, 'sitemap' ), __( 'List these archives in the sitemap (never when noindex)', 'orbitstem-seo' ) ); ?>

__( 'Author archives', 'orbitstem-seo' ), 'date_archive' => __( 'Date archives', 'orbitstem-seo' ), ) as $key => $label ) { $group = (array) $s->get( $key ); /* translators: %s: archive kind */ $this->text_row( sprintf( __( '%s: title', 'orbitstem-seo' ), $label ), $opt . '[' . $key . '][title]', $group['title'] ); /* translators: %s: archive kind */ $this->text_row( sprintf( __( '%s: description', 'orbitstem-seo' ), $label ), $opt . '[' . $key . '][description]', $group['description'] ); $this->checkbox_row( $label, $opt . '[' . $key . '][noindex]', $group['noindex'], __( 'Hide from search results (noindex)', 'orbitstem-seo' ) ); } $this->text_row( __( 'Search results title', 'orbitstem-seo' ), $opt . '[search_title]', $s->get( 'search_title' ) ); $this->text_row( __( '404 page title', 'orbitstem-seo' ), $opt . '[not_found_title]', $s->get( 'not_found_title' ) ); /* translators: %d: number of empty category, tag and other archives */ $this->checkbox_row( __( 'Empty archives', 'orbitstem-seo' ), $opt . '[noindex_empty_terms]', $s->get( 'noindex_empty_terms' ), sprintf( __( 'Hide category, tag and other archives that have no posts (noindex). Affects %d archives right now.', 'orbitstem-seo' ), $this->empty_terms() ) ); ?>

checkbox_row( __( 'XML sitemap', 'orbitstem-seo' ), $opt . '[sitemap_enabled]', $s->get( 'sitemap_enabled' ), __( 'Use the WordPress sitemap, leaving out noindexed items and items with a custom canonical URL. Unticking turns sitemaps off entirely.', 'orbitstem-seo' ) ); ?> checkbox_row( __( 'Image sitemap', 'orbitstem-seo' ), $opt . '[image_sitemap]', $s->get( 'image_sitemap' ), __( 'List the images of each page (featured image and images in the content) so they can appear in image search.', 'orbitstem-seo' ) ); $this->checkbox_row( __( 'Video sitemap', 'orbitstem-seo' ), $opt . '[video_sitemap]', $s->get( 'video_sitemap' ), __( 'List pages with YouTube videos or video schema so they can appear in video results.', 'orbitstem-seo' ) ); $this->checkbox_row( __( 'News sitemap', 'orbitstem-seo' ), $opt . '[news_sitemap]', $s->get( 'news_sitemap' ), __( 'For news publishers in Google News: lists posts from the last two days. Off by default.', 'orbitstem-seo' ) ); $this->text_row( __( 'News publication name', 'orbitstem-seo' ), $opt . '[news_name]', $s->get( 'news_name' ), __( 'As registered in Google News. Empty uses the site title.', 'orbitstem-seo' ) ); ?>

checkbox_row( __( 'JSON-LD', 'orbitstem-seo' ), $opt . '[schema_enabled]', $s->get( 'schema_enabled' ), __( 'Output schema.org structured data', 'orbitstem-seo' ) ); $this->checkbox_row( __( 'Videos', 'orbitstem-seo' ), $opt . '[video_schema]', $s->get( 'video_schema' ), __( 'Add video schema for YouTube videos embedded in posts.', 'orbitstem-seo' ) ); if ( \Orbitstem\SEO\Schema\Woo::active() ) { $this->checkbox_row( __( 'WooCommerce', 'orbitstem-seo' ), $opt . '[woo_schema]', $s->get( 'woo_schema' ), __( 'Full Product schema for products: price or price range, stock, SKU, brand, GTIN, ratings and reviews. Replaces WooCommerce\'s simpler product markup.', 'orbitstem-seo' ) ); } else { printf( '', esc_attr( $opt . '[woo_schema]' ), $s->get( 'woo_schema' ) ? '1' : '0' ); } $this->select_row( __( 'This site represents', 'orbitstem-seo' ), $opt . '[knowledge_type]', $s->get( 'knowledge_type' ), array( 'organization' => __( 'An organization', 'orbitstem-seo' ), 'person' => __( 'A person', 'orbitstem-seo' ), ) ); $this->text_row( __( 'Organization or person name', 'orbitstem-seo' ), $opt . '[knowledge_name]', $s->get( 'knowledge_name' ), __( 'Empty uses the site title.', 'orbitstem-seo' ) ); $this->text_row( __( 'Logo or photo URL', 'orbitstem-seo' ), $opt . '[knowledge_logo]', $s->get( 'knowledge_logo' ), '', 'url' ); ?>

[orbitstem_breadcrumbs]', 'orbitstem_seo_breadcrumbs()' ); ?>

text_row( __( 'Home label', 'orbitstem-seo' ), $opt . '[breadcrumb_home]', $s->get( 'breadcrumb_home' ), __( 'Empty uses "Home".', 'orbitstem-seo' ) ); $this->text_row( __( 'Separator', 'orbitstem-seo' ), $opt . '[breadcrumb_separator]', $s->get( 'breadcrumb_separator' ) ); ?>

checkbox_row( __( 'External links', 'orbitstem-seo' ), $opt . '[check_external_links]', $s->get( 'check_external_links' ), __( 'Also check links to other websites during scans. This sends a request from your server to each linked site (one per address, at most weekly). Off by default.', 'orbitstem-seo' ) ); $this->checkbox_row( __( '404 log', 'orbitstem-seo' ), $opt . '[not_found_log]', $s->get( 'not_found_log' ), __( 'Record addresses that return "page not found" (path, hit count and referring page only; no IP addresses).', 'orbitstem-seo' ) ); $this->checkbox_row( __( 'Redirect query strings', 'orbitstem-seo' ), $opt . '[redirect_pass_query]', $s->get( 'redirect_pass_query' ), __( 'When a redirect runs, pass the visitor\'s query string (for example ?utm_source=newsletter) on to the new address.', 'orbitstem-seo' ) ); ?> checkbox_row( __( 'Guardian', 'orbitstem-seo' ), $opt . '[guardian_auto_redirect]', $s->get( 'guardian_auto_redirect' ), __( 'When a published item gets a new address, redirect the old address to it (301).', 'orbitstem-seo' ) ); $this->select_row( __( 'Monitor snapshots', 'orbitstem-seo' ), $opt . '[monitor_frequency]', $s->get( 'monitor_frequency' ), array( 'weekly' => __( 'Weekly', 'orbitstem-seo' ), 'daily' => __( 'Daily', 'orbitstem-seo' ), 'off' => __( 'Off (manual only)', 'orbitstem-seo' ), ) ); $this->checkbox_row( __( 'Monitor alerts', 'orbitstem-seo' ), $opt . '[monitor_email]', $s->get( 'monitor_email' ), /* translators: %s: email address */ sprintf( __( 'Email %s when a snapshot finds unexplained errors or warnings.', 'orbitstem-seo' ), get_option( 'admin_email' ) ) ); ?> checkbox_row( __( 'Update guard', 'orbitstem-seo' ), $opt . '[update_guard]', $s->get( 'update_guard' ), __( 'After every theme, plugin or WordPress update, check ten key pages and warn right away if the update removed a title, broke a canonical, added noindex or a second H1.', 'orbitstem-seo' ) ); $this->select_row( __( 'SEO health email', 'orbitstem-seo' ), $opt . '[health_email]', $s->get( 'health_email' ), array( 'off' => __( 'Off', 'orbitstem-seo' ), 'weekly' => __( 'Weekly', 'orbitstem-seo' ), 'monthly' => __( 'Monthly', 'orbitstem-seo' ), ) ); ?>
plugin->local->details(); $lbase = $opt . '[local]'; ?>

checkbox_row( __( 'Local SEO', 'orbitstem-seo' ), $lbase . '[enabled]', $local['enabled'], __( 'This site represents a local business', 'orbitstem-seo' ) ); $this->select_row( __( 'Business type', 'orbitstem-seo' ), $lbase . '[type]', $local['type'], \Orbitstem\SEO\Schema\LocalBusiness::types() ); $this->text_row( __( 'Business name', 'orbitstem-seo' ), $lbase . '[name]', $local['name'], __( 'Empty uses the site title.', 'orbitstem-seo' ) ); $this->text_row( __( 'Street address', 'orbitstem-seo' ), $lbase . '[street]', $local['street'] ); $this->text_row( __( 'City', 'orbitstem-seo' ), $lbase . '[city]', $local['city'] ); $this->text_row( __( 'State or region', 'orbitstem-seo' ), $lbase . '[region]', $local['region'] ); $this->text_row( __( 'Postal code', 'orbitstem-seo' ), $lbase . '[postal]', $local['postal'] ); $this->text_row( __( 'Country code', 'orbitstem-seo' ), $lbase . '[country]', $local['country'], __( 'Two letters, for example US, GB or IN.', 'orbitstem-seo' ) ); $this->text_row( __( 'Phone', 'orbitstem-seo' ), $lbase . '[phone]', $local['phone'] ); $this->text_row( __( 'Email', 'orbitstem-seo' ), $lbase . '[email]', $local['email'] ); $this->text_row( __( 'Price range', 'orbitstem-seo' ), $lbase . '[price_range]', $local['price_range'], __( 'For example $$ or 10–50 EUR.', 'orbitstem-seo' ) ); $this->text_row( __( 'Latitude', 'orbitstem-seo' ), $lbase . '[lat]', $local['lat'], __( 'Optional. In Google Maps, right-click your location to copy its coordinates.', 'orbitstem-seo' ) ); $this->text_row( __( 'Longitude', 'orbitstem-seo' ), $lbase . '[lng]', $local['lng'] ); ?> plugin->local->missing() ) : ?>

plugin->local->missing() ) ); ?>

robots.txt' ); ?>

checkbox_row( __( 'llms.txt', 'orbitstem-seo' ), $opt . '[llms_txt]', $s->get( 'llms_txt' ), sprintf( __( 'Publish %s: a short, plain guide to your site (key pages and recent posts with their descriptions) that AI tools can read to understand and quote it correctly.', 'orbitstem-seo' ), home_url( '/llms.txt' ) ) ); if ( ! \Orbitstem\SEO\Frontend\Robots::looks_like_staging() ) { $staging_text = __( 'On a copy of your site that WordPress knows is not live (WP_ENVIRONMENT_TYPE set to staging, development or local, or a localhost or .test address), ask search engines not to index any page. This site is set up as a live site, so nothing changes here.', 'orbitstem-seo' ); } elseif ( ! $this->plugin->output_enabled() ) { /* translators: %s: reason */ $staging_text = sprintf( __( 'This site is a staging, development or local copy (%s), but another SEO plugin is active, so Orbitstem SEO is in safe mode and this does nothing for now. Without the other plugin, every page asks search engines not to index it while this is ticked.', 'orbitstem-seo' ), \Orbitstem\SEO\Frontend\Robots::staging_reason() ); } else { /* translators: %s: reason */ $staging_text = sprintf( __( 'This site is a staging, development or local copy (%s), so while this is ticked every page asks search engines not to index it. Untick only if this is your live site.', 'orbitstem-seo' ), \Orbitstem\SEO\Frontend\Robots::staging_reason() ); } $this->checkbox_row( __( 'Staging protection', 'orbitstem-seo' ), $opt . '[staging_guard]', $s->get( 'staging_guard' ), $staging_text ); ?>

History with a Rollback button. Repairs that remove content are never automatic.', 'orbitstem-seo' ); ?>

select_row( __( 'SEO Issues scan', 'orbitstem-seo' ), $opt . '[scan_frequency]', $s->get( 'scan_frequency' ), array( 'weekly' => __( 'Weekly', 'orbitstem-seo' ), 'daily' => __( 'Daily', 'orbitstem-seo' ), 'off' => __( 'Off (manual only)', 'orbitstem-seo' ), ) ); $this->checkbox_row( __( 'Fix redirected links', 'orbitstem-seo' ), $opt . '[auto_redirected_links]', $s->get( 'auto_redirected_links' ), __( 'Point internal links that go through a permanent redirect (301 or 308) straight at the final page. Links behind temporary redirects are left for you to review.', 'orbitstem-seo' ) ); $this->checkbox_row( __( 'Flatten redirect chains', 'orbitstem-seo' ), $opt . '[auto_redirect_chains]', $s->get( 'auto_redirect_chains' ), __( 'When an Orbitstem redirect leads to another redirect, send it straight to the final address. Loops are never changed automatically.', 'orbitstem-seo' ) ); $this->checkbox_row( __( 'Repair summary', 'orbitstem-seo' ), $opt . '[auto_repair_email]', $s->get( 'auto_repair_email' ), /* translators: %s: email address */ sprintf( __( 'Email %s a summary when automatic repairs change something.', 'orbitstem-seo' ), get_option( 'admin_email' ) ) ); ?>

checkbox_row( __( 'IndexNow', 'orbitstem-seo' ), $opt . '[indexnow_enabled]', $s->get( 'indexnow_enabled' ), __( 'Notify search engines that support IndexNow (for example Microsoft Bing) as soon as you publish, update, move or remove content, so they recrawl it sooner. Off by default.', 'orbitstem-seo' ) ); ?> render_indexnow_status(); ?>

checkbox_row( __( 'Other SEO plugins', 'orbitstem-seo' ), $opt . '[force_output]', $s->get( 'force_output' ), __( 'Print Orbitstem SEO tags even when another SEO plugin is active (may cause duplicate tags)', 'orbitstem-seo' ) ); $this->checkbox_row( __( 'Uninstall', 'orbitstem-seo' ), $opt . '[delete_data_on_uninstall]', $s->get( 'delete_data_on_uninstall' ), __( 'Delete all Orbitstem SEO settings and per-item SEO fields when the plugin is deleted. Deactivating never deletes data.', 'orbitstem-seo' ) ); ?>

Media Rename — OrbitStem' ); ?>

get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status = 'applied' AND fixer IN ('redirected_links','redirect_chains','broken_links','outdated_links','orphan_links')", \Orbitstem\SEO\Diagnostics\Tables::changes() ) ); if ( $fixed < 10 ) { return; } $link = function ( $answer ) { return wp_nonce_url( admin_url( 'admin-post.php?action=orbitstem_seo_review&answer=' . $answer ), 'orbitstem_seo_review' ); }; ?>

time() + 30 * DAY_IN_SECONDS ), false ); } else { update_option( 'orbitstem_seo_review_state', array( 'done' => time() ), false ); } if ( 'review' === $answer ) { wp_redirect( 'https://wordpress.org/support/plugin/orbitstem-seo/reviews/#new-post' ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- the plugin's own review page on WordPress.org. exit; } wp_safe_redirect( admin_url( 'admin.php?page=' . self::SLUG ) ); exit; } /** * Number of empty category, tag and other archives Orbitstem manages. * * @return int */ private function empty_terms() { $taxonomies = array_keys( $this->plugin->types->taxonomies() ); if ( ! $taxonomies ) { return 0; } return (int) wp_count_terms( array( 'taxonomy' => $taxonomies, 'hide_empty' => false, ) ) - (int) wp_count_terms( array( 'taxonomy' => $taxonomies, 'hide_empty' => true, ) ); } /** * What IndexNow sends and to whom, its state, and recent submissions. */ private function render_indexnow_status() { $indexnow = $this->plugin->indexnow; ?>

api.indexnow.org', '' . esc_html__( 'IndexNow terms', 'orbitstem-seo' ) . '' ); ?>

plugin->settings->get( 'indexnow_enabled' ) ) { return; } $status = $indexnow->status(); $log = array_slice( $indexnow->log(), 0, 10 ); $queued = count( $indexnow->queued() ); ?>

key_url() ); ?>

3 ) { /* translators: %d: number of further addresses */ echo ' ' . esc_html( sprintf( __( 'and %d more', 'orbitstem-seo' ), (int) $entry['count'] - 3 ) ); } ?>

" />

$options Value => label. */ private function select_row( $label, $name, $value, array $options ) { $id = sanitize_html_class( str_replace( array( '[', ']' ), '-', $name ) ); ?>