'string', 'single' => true, 'show_in_rest' => false, 'sanitize_callback' => 'sts_seo_plain_text', 'auth_callback' => 'sts_seo_meta_permission', ) ); register_post_meta( $type, '_sts_seo_description', array( 'type' => 'string', 'single' => true, 'show_in_rest' => false, 'sanitize_callback' => 'sts_seo_plain_text', 'auth_callback' => 'sts_seo_meta_permission', ) ); register_post_meta( $type, '_sts_seo_social_image_id', array( 'type' => 'integer', 'single' => true, 'show_in_rest' => false, 'sanitize_callback' => 'sts_seo_storable_image_id', 'auth_callback' => 'sts_seo_meta_permission', ) ); } } /** * Meta auth callback: check access to the specific content object. * * @param bool $allowed Whether the user can add the meta (ignored; capability decides). * @param string $meta_key Meta key. * @param int $post_id Post ID. * @return bool */ function sts_seo_meta_permission( $allowed, $meta_key, $post_id ) { return current_user_can( 'edit_post', $post_id ); } /** * Register a small meta box supported by both core editors. */ function sts_seo_add_meta_boxes() { foreach ( array( 'post', 'page' ) as $type ) { add_meta_box( 'sts-seo', __( 'STS SEO', 'sts-seo' ), 'sts_seo_meta_box', $type, 'normal', 'default', array( '__block_editor_compatible_meta_box' => true ) ); } } /** * Render the shared native media selector. * * @param string $name Form field name; also the base of element ids. * @param int|string $id Stored attachment ID. * @param string $label Visible label. * @param string $help_id Optional id of a help paragraph that also describes the Select button. */ function sts_seo_image_control( $name, $id, $label, $help_id = '' ) { $id = sts_seo_storable_image_id( $id ); $preview = $id ? wp_get_attachment_image_url( $id, 'medium' ) : ''; ?>

<?php esc_attr_e( 'Selected social image preview', 'sts-seo' ); ?> />

ID, '_sts_seo_social_image_id', true ), __( 'Social Image', 'sts-seo' ) ); ?>

post_type, array( 'post', 'page' ), true ) || wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) { return false; } return current_user_can( 'edit_post', $post_id ); } /** * Save only submitted fields; Quick Edit, REST and unrelated saves retain metadata. * * @param int $post_id Post ID. * @param WP_Post $post Post object. */ function sts_seo_save_meta( $post_id, $post ) { if ( ! sts_seo_can_save( $post_id, $post ) ) { return; } // The nonce field is present only when the STS SEO meta box was submitted. if ( ! isset( $_POST['sts_seo_nonce'] ) || ! is_string( $_POST['sts_seo_nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['sts_seo_nonce'] ) ), 'sts_seo_save_meta' ) ) { return; } foreach ( array( 'sts_seo_title' => '_sts_seo_title', 'sts_seo_description' => '_sts_seo_description', ) as $field => $meta_key ) { if ( ! isset( $_POST[ $field ] ) || ! is_string( $_POST[ $field ] ) ) { continue; } // Normalized to plain text (markup and control characters removed); escaped on output. $value = sts_seo_normalize_field( wp_unslash( $_POST[ $field ] ) ); if ( '' === $value ) { delete_post_meta( $post_id, $meta_key ); } else { update_post_meta( $post_id, $meta_key, wp_slash( $value ) ); } } if ( isset( $_POST['sts_seo_social_image_id'] ) && is_scalar( $_POST['sts_seo_social_image_id'] ) ) { $value = sts_seo_storable_image_id( wp_unslash( $_POST['sts_seo_social_image_id'] ) ); if ( $value && ! current_user_can( 'upload_files' ) ) { return; } if ( $value ) { update_post_meta( $post_id, '_sts_seo_social_image_id', $value ); } else { delete_post_meta( $post_id, '_sts_seo_social_image_id' ); } } } /** * Add the minimal settings screen under Settings. */ function sts_seo_admin_menu() { add_options_page( __( 'STS SEO', 'sts-seo' ), __( 'STS SEO', 'sts-seo' ), 'manage_options', 'sts-seo', 'sts_seo_settings_page' ); } /** * Register the two global settings and their fields. */ function sts_seo_register_settings() { register_setting( 'sts_seo', 'sts_seo_default_social_image_id', array( 'type' => 'integer', 'sanitize_callback' => 'sts_seo_storable_image_id', 'default' => 0, 'show_in_rest' => false, ) ); add_settings_section( 'sts_seo_social', __( 'Social sharing', 'sts-seo' ), '__return_false', 'sts-seo' ); add_settings_field( 'sts_seo_default_social_image_id', __( 'Default social image', 'sts-seo' ), 'sts_seo_default_image_field', 'sts-seo', 'sts_seo_social' ); register_setting( 'sts_seo', 'sts_seo_business_schema', array( 'type' => 'array', 'sanitize_callback' => 'sts_seo_sanitize_schema_settings', 'default' => array(), 'show_in_rest' => false, ) ); add_settings_section( 'sts_seo_schema', __( 'Business structured data', 'sts-seo' ), 'sts_seo_schema_section', 'sts-seo' ); foreach ( sts_seo_schema_field_definitions() as $key => $field ) { add_settings_field( 'sts_seo_schema_' . $key, $field['label'], 'sts_seo_schema_field', 'sts-seo', 'sts_seo_schema', array( 'key' => $key, 'label_for' => 'sts-seo-schema-' . str_replace( '_', '-', $key ), ) ); } } /** * Explain the single reusable entity and its ownership boundary. */ function sts_seo_schema_section() { echo '

' . esc_html__( 'Optional. Configure one Organization, LocalBusiness, or Restaurant entity. Empty and invalid fields are omitted; ratings, reviews, coordinates, and social profiles are never inferred.', 'sts-seo' ) . '

'; } /** * Settings-screen field labels, types and guidance. * * @return array Field definitions keyed by setting key. */ function sts_seo_schema_field_definitions() { return array( 'type' => array( 'label' => __( 'Schema type', 'sts-seo' ), 'type' => 'select', 'help' => __( 'Disabled outputs no business structured data.', 'sts-seo' ), ), 'name' => array( 'label' => __( 'Business name', 'sts-seo' ), 'type' => 'text', ), 'url' => array( 'label' => __( 'Business URL', 'sts-seo' ), 'type' => 'url', ), 'telephone' => array( 'label' => __( 'Telephone', 'sts-seo' ), 'type' => 'tel', ), 'street_address' => array( 'label' => __( 'Street address', 'sts-seo' ), 'type' => 'text', ), 'address_locality' => array( 'label' => __( 'City / locality', 'sts-seo' ), 'type' => 'text', ), 'address_region' => array( 'label' => __( 'State / region', 'sts-seo' ), 'type' => 'text', ), 'postal_code' => array( 'label' => __( 'Postal code', 'sts-seo' ), 'type' => 'text', ), 'address_country' => array( 'label' => __( 'Country', 'sts-seo' ), 'type' => 'text', 'help' => __( 'Use a two-letter country code when known, for example US.', 'sts-seo' ), ), 'cuisine' => array( 'label' => __( 'Cuisine', 'sts-seo' ), 'type' => 'textarea', 'help' => __( 'Restaurant only. Enter one cuisine per line or separate values with commas.', 'sts-seo' ), ), 'menu_url' => array( 'label' => __( 'Menu URL', 'sts-seo' ), 'type' => 'url', 'help' => __( 'Restaurant only.', 'sts-seo' ), ), 'founding_year' => array( 'label' => __( 'Founding year', 'sts-seo' ), 'type' => 'text', 'help' => __( 'Optional four-digit year.', 'sts-seo' ), ), 'opening_hours' => array( 'label' => __( 'Opening hours', 'sts-seo' ), 'type' => 'textarea', 'help' => __( 'One interval per line: Monday,Tuesday|11:00|14:00. Add another line for split hours. Use English day names and 24-hour HH:MM times.', 'sts-seo' ), ), ); } /** * Render one accessible Settings API field. * * @param array $args Field arguments; `key` names the setting. */ function sts_seo_schema_field( $args ) { $key = isset( $args['key'] ) ? $args['key'] : ''; $definitions = sts_seo_schema_field_definitions(); if ( ! isset( $definitions[ $key ] ) ) { return; } $field = $definitions[ $key ]; $value = sts_seo_schema_setting_value( $key ); $name = 'sts_seo_business_schema[' . $key . ']'; $id = 'sts-seo-schema-' . str_replace( '_', '-', $key ); $help = ! empty( $field['help'] ) ? $id . '-help' : ''; // Screen readers announce the help text with the field (WCAG 1.3.1, 3.3.2); escaped where it is printed. if ( 'select' === $field['type'] ) { echo ''; } elseif ( 'textarea' === $field['type'] ) { echo ''; } else { echo ''; } if ( $help ) { echo '

' . esc_html( $field['help'] ) . '

'; } } /** * Render the global default media selector. */ function sts_seo_default_image_field() { sts_seo_image_control( 'sts_seo_default_social_image_id', get_option( 'sts_seo_default_social_image_id', 0 ), __( 'Site-wide fallback image', 'sts-seo' ), 'sts-seo-default-image-help' ); echo '

' . esc_html__( 'Optional. Used only when content has neither a valid explicit social image nor a valid featured image. The Site Icon is not automatically used.', 'sts-seo' ) . '

'; } /** * Render the settings page. * * The Settings API owns the nonce and option saving; the capability is checked * here too. WordPress already prints the "Settings saved" notice on pages under * Settings, so this page does not call settings_errors() a second time. */ function sts_seo_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } ?>

id || ( 'post' === $screen->base && in_array( $screen->post_type, array( 'post', 'page' ), true ) ) ); } /** * Enqueue the admin script and styles on supported screens only. * * Uses the core media UI; the plugin's own small script is vanilla JavaScript. */ function sts_seo_admin_assets() { if ( ! sts_seo_admin_screen() ) { return; } $deps = array(); if ( current_user_can( 'upload_files' ) ) { wp_enqueue_media(); $deps[] = 'media-views'; } wp_enqueue_script( 'sts-seo-admin', plugins_url( 'assets/admin.js', STS_SEO_FILE ), $deps, STS_SEO_VERSION, true ); wp_localize_script( 'sts-seo-admin', 'stsSeoAdmin', array( 'choose' => __( 'Select social image', 'sts-seo' ), 'use' => __( 'Use this image', 'sts-seo' ), 'empty' => __( 'No explicit image selected.', 'sts-seo' ), 'invalid' => __( 'Please select an image attachment.', 'sts-seo' ), /* translators: %s: Number of Unicode characters. */ 'count' => __( '%s characters — guidance only.', 'sts-seo' ), 'selected' => __( 'Social image selected.', 'sts-seo' ), /* translators: 1: Field name, 2: Number of Unicode characters. */ 'status' => __( '%1$s: %2$s characters — guidance only.', 'sts-seo' ), /* translators: 1: Field name, 2: Approximate guidance length in characters. */ 'guidance' => __( '%1$s is now longer than about %2$s characters. This is guidance only; search engines may show more or less.', 'sts-seo' ), ) ); wp_enqueue_style( 'sts-seo-admin', plugins_url( 'assets/admin.css', STS_SEO_FILE ), array(), STS_SEO_VERSION ); } /** * Informational notices only; no competing indexing toggle or automatic takeover. * * Every notice appears on Settings → STS SEO only, so post and page editors are * never shown a persistent, non-dismissible notice on every screen. */ function sts_seo_admin_notices() { $screen = get_current_screen(); if ( ! $screen || 'settings_page_sts-seo' !== $screen->id || ! current_user_can( 'manage_options' ) ) { return; } if ( ! get_option( 'blog_public' ) ) { echo '

' . esc_html__( 'Search engine visibility is currently disabled in WordPress. STS SEO will respect this setting and will not override it.', 'sts-seo' ) . '

'; } $conflicts = sts_seo_conflicts(); if ( $conflicts ) { /* translators: %s: Names of other active metadata providers. */ echo '

' . esc_html( sprintf( __( 'STS SEO frontend output is suppressed because another metadata provider may be active: %s. Stored STS SEO fields are preserved. Review ownership before using both plugins.', 'sts-seo' ), implode( ', ', $conflicts ) ) ) . '

'; return; } // Per-feature ownership. $labels = array( 'title' => __( 'SEO titles', 'sts-seo' ), 'description' => __( 'search descriptions', 'sts-seo' ), 'social' => __( 'social sharing tags', 'sts-seo' ), 'posts_canonical' => __( 'the posts-page canonical link', 'sts-seo' ), 'schema' => __( 'business structured data', 'sts-seo' ), ); foreach ( sts_seo_features() as $feature ) { $owner = sts_seo_feature_owner( $feature ); if ( '' !== $owner && isset( $labels[ $feature ] ) ) { /* translators: 1: Feature name, 2: Plugin name. */ echo '

' . esc_html( sprintf( __( 'STS SEO leaves %1$s to %2$s on this site.', 'sts-seo' ), $labels[ $feature ], $owner ) ) . '

'; } } if ( class_exists( 'Jetpack' ) && is_callable( array( 'Jetpack', 'is_module_active' ) ) && ( Jetpack::is_module_active( 'sharedaddy' ) || Jetpack::is_module_active( 'publicize' ) ) && sts_seo_manages_jetpack_social() ) { echo '

' . esc_html__( 'STS SEO provides the social sharing tags on posts, pages and the posts page, so Jetpack\'s Open Graph tags are turned off there (Jetpack still provides them elsewhere). Nothing is changed in Jetpack\'s settings. To keep Jetpack\'s tags instead, a developer can return false from the sts_seo_jetpack_social filter.', 'sts-seo' ) . '

'; } }