Plugin Directory

Changeset 3798783


Ignore:
Timestamp:
11/12/2026 04:25:55 PM (less than one hour ago)
Author:
andrewmurray
Message:

3.1.7

  • New (Pro): "Your Focus Keywords" on the WordPress dashboard widget shows each post's focus keyword with its Google position, a trend line and the change over time, using history SEOMedic already records daily (no extra Google requests). Keywords without a position yet are counted, never given a made-up number.
Location:
seomedic-seo-link-auditor/trunk
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • seomedic-seo-link-auditor/trunk/admin/css/dashboard-widget.css

    r3798411 r3798783  
    627627        color: #646970;
    628628}
     629
     630/* Your Focus Keywords (3.1.7) — rows reuse .seomedic-mover-row */
     631.seomedic-focus-list {
     632        margin: 0 0 10px;
     633}
     634.seomedic-focus-list .seomedic-mover-query a {
     635        color: inherit;
     636        text-decoration: none;
     637}
     638.seomedic-focus-list .seomedic-mover-query a:hover {
     639        color: #2271b1;
     640        text-decoration: underline;
     641}
     642.seomedic-mover-pill.seomedic-mover-flat {
     643        background: #f0f0f1;
     644        color: #50575e;
     645}
  • seomedic-seo-link-auditor/trunk/changelog.txt

    r3798411 r3798783  
    66
    77== Changelog ==
     8
     9= 3.1.7 =
     10* Dashboard widget (Pro): "Your Focus Keywords", under Money Pages. `SEOMedic_GSC_Integration::get_focus_keyword_tracking()` matches each published post's effective focus keyword (Yoast, then Rank Math primary, then ours; `get_focus_keyword_posts()`, ≤ 500 posts, cached an hour in `seomedic_focus_kw_posts`, cleared by `update_focus_keyword()` and both uninstall paths) against the keyword snapshots the daily cron already records — no Google calls. Match is exact after `norm_keyword()` (case, entities, curly quotes, whitespace) or same content words in any order (stopwords mirror `utils/focusKeywordMatch.ts`); related queries and plurals never count; exact beats variant. Pairs in an older snapshot but not the latest are "dropped" and always get a row. Positions are trailing 90-day averages and the block says so; keywords with no data are counted as "no position yet" (below #30, under 10 impressions, or outside the top 500 pairs), never shown as a number. Harness tests/focus-keyword-tracking-harness.php (30).
    811
    912= 3.1.6 =
  • seomedic-seo-link-auditor/trunk/includes/class-dashboard-widget.php

    r3798411 r3798783  
    427427                        <?php self::render_top_by_clicks( $top30, $top30_state ); ?>
    428428                        <?php self::render_money_pages(); ?>
     429                        <?php self::render_focus_keywords(); ?>
    429430                        <?php self::render_movers( $movers ); ?>
    430431
    … …  
    996997        }
    997998
     999        /**
     1000         * "Your Focus Keywords": each post's focus keyword with its Google position
     1001         * and trend, read from the keyword snapshots the daily cron already records
     1002         * (SEOMedic_GSC_Integration::get_focus_keyword_tracking()). No Google call.
     1003         *
     1004         * Positions are trailing 90-day averages, which the hint line says, and a
     1005         * keyword missing from the data is "no position yet", never a number: the
     1006         * snapshot also leaves out pairs under 10 impressions or outside the site's
     1007         * top 500, so "below #30" would overclaim.
     1008         * Renders nothing when no post has a focus keyword: no nagging.
     1009         */
     1010        private static function render_focus_keywords() {
     1011                if ( ! method_exists( 'SEOMedic_GSC_Integration', 'get_focus_keyword_tracking' ) ) {
     1012                        return;
     1013                }
     1014                $t = SEOMedic_GSC_Integration::get_focus_keyword_tracking();
     1015                if ( empty( $t['set'] ) ) {
     1016                        return;
     1017                }
     1018                $rows    = isset( $t['rows'] ) && is_array( $t['rows'] ) ? $t['rows'] : array();
     1019                $tracked = array_values( array_filter( $rows, function ( $r ) {
     1020                        return 'tracked' === $r['status'];
     1021                } ) );
     1022                $dropped = array_values( array_filter( $rows, function ( $r ) {
     1023                        return 'dropped' === $r['status'];
     1024                } ) );
     1025                // Up to two that fell out of the data always get a row: they are the
     1026                // ones worth noticing, and sorting by impressions would bury them.
     1027                $show_dropped = array_slice( $dropped, 0, 2 );
     1028                $show         = array_merge( array_slice( $tracked, 0, self::FOCUS_ROWS - count( $show_dropped ) ), $show_dropped );
     1029                $missing      = max( 0, (int) $t['set'] - count( $tracked ) - count( $dropped ) );
     1030                ?>
     1031                <h4 class="seomedic-widget-section-title">
     1032                        <?php esc_html_e( 'Your Focus Keywords', 'seomedic-seo-link-auditor' ); ?>
     1033                        <span class="seomedic-money-count">
     1034                                <?php
     1035                                echo esc_html( sprintf(
     1036                                        /* translators: 1: posts with a focus keyword, 2: how many of those have position data */
     1037                                        __( '%1$s set · %2$s with a position', 'seomedic-seo-link-auditor' ),
     1038                                        number_format_i18n( (int) $t['set'] ),
     1039                                        number_format_i18n( count( $tracked ) )
     1040                                ) );
     1041                                ?>
     1042                        </span>
     1043                </h4>
     1044                <?php if ( '' === $t['asOf'] ) : ?>
     1045                        <p class="seomedic-movers-empty">
     1046                                <?php esc_html_e( 'Positions appear here after the first daily sync with Google.', 'seomedic-seo-link-auditor' ); ?>
     1047                        </p>
     1048                        <?php
     1049                        return;
     1050                endif;
     1051                ?>
     1052                <p class="seomedic-widget-hint">
     1053                        <?php esc_html_e( 'Average Google position over the last 90 days, recorded daily, for the keyword each post targets.', 'seomedic-seo-link-auditor' ); ?>
     1054                </p>
     1055                <?php if ( empty( $show ) ) : ?>
     1056                        <p class="seomedic-movers-empty">
     1057                                <?php esc_html_e( 'No position for these keywords yet. Google shows their pages for them below #30, fewer than 10 times, or not at all.', 'seomedic-seo-link-auditor' ); ?>
     1058                        </p>
     1059                <?php else : ?>
     1060                        <div class="seomedic-focus-list">
     1061                                <?php foreach ( $show as $r ) : ?>
     1062                                        <?php self::render_focus_row( $r ); ?>
     1063                                <?php endforeach; ?>
     1064                        </div>
     1065                <?php endif; ?>
     1066                <?php if ( $missing > 0 && ! empty( $show ) ) : ?>
     1067                        <p class="seomedic-fresh-more">
     1068                                <?php
     1069                                echo esc_html( sprintf(
     1070                                        /* translators: %s: number of focus keywords */
     1071                                        _n(
     1072                                                '%s other focus keyword has no position yet (below #30, fewer than 10 impressions, or not shown at all).',
     1073                                                '%s other focus keywords have no position yet (below #30, fewer than 10 impressions, or not shown at all).',
     1074                                                $missing,
     1075                                                'seomedic-seo-link-auditor'
     1076                                        ),
     1077                                        number_format_i18n( $missing )
     1078                                ) );
     1079                                ?>
     1080                        </p>
     1081                <?php endif; ?>
     1082                <?php
     1083        }
     1084
     1085        /** Rows in "Your Focus Keywords". */
     1086        const FOCUS_ROWS = 6;
     1087
     1088        /**
     1089         * One focus-keyword row: keyword (links to the editor), sparkline, position
     1090         * and change. Shares the Biggest Movers row styles.
     1091         *
     1092         * @param array $r One row from get_focus_keyword_tracking().
     1093         */
     1094        private static function render_focus_row( $r ) {
     1095                $id      = (int) $r['postId'];
     1096                $title   = html_entity_decode( get_the_title( $id ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
     1097                $keyword = html_entity_decode( (string) $r['keyword'], ENT_QUOTES | ENT_HTML5, 'UTF-8' );
     1098                $edit    = get_edit_post_link( $id, 'raw' );
     1099                $delta   = isset( $r['delta'] ) ? $r['delta'] : null;
     1100                $points  = isset( $r['points'] ) && is_array( $r['points'] ) ? $r['points'] : array();
     1101                $dropped = 'dropped' === $r['status'];
     1102
     1103                $tip = $title;
     1104                if ( 'variant' === $r['match'] ) {
     1105                        /* translators: %s: the Google search the position belongs to */
     1106                        $tip .= ' — ' . sprintf( __( 'Google search “%s” (same words, different order)', 'seomedic-seo-link-auditor' ), $r['query'] );
     1107                }
     1108                if ( $dropped ) {
     1109                        /* translators: 1: last position, 2: date */
     1110                        $tip .= ' — ' . sprintf( __( 'Dropped out of the data: now below #30 or under 10 impressions. Last seen at #%1$s on %2$s.', 'seomedic-seo-link-auditor' ), number_format_i18n( (float) $r['position'], 1 ), $r['lastSeen'] );
     1111                } elseif ( null !== $delta ) {
     1112                        /* translators: %s: date the history starts */
     1113                        $tip .= ' — ' . sprintf( __( 'Change since %s', 'seomedic-seo-link-auditor' ), $r['since'] );
     1114                }
     1115
     1116                // Lower position number = higher in Google, so a negative delta is up.
     1117                if ( $dropped ) {
     1118                        $dir   = 'down';
     1119                        $pill  = __( 'dropped', 'seomedic-seo-link-auditor' );
     1120                        $color = '#d63638';
     1121                } elseif ( null === $delta ) {
     1122                        $dir   = 'flat';
     1123                        $pill  = __( 'new', 'seomedic-seo-link-auditor' );
     1124                        $color = '#8c8f94';
     1125                } elseif ( abs( $delta ) < 0.5 ) {
     1126                        $dir   = 'flat';
     1127                        $pill  = '=';
     1128                        $color = '#8c8f94';
     1129                } else {
     1130                        $dir   = $delta < 0 ? 'up' : 'down';
     1131                        $pill  = ( $delta < 0 ? '▲ ' : '▼ ' ) . number_format_i18n( abs( $delta ), 1 );
     1132                        $color = $delta < 0 ? '#00a32a' : '#d63638';
     1133                }
     1134                ?>
     1135                <div class="seomedic-mover-row" title="<?php echo esc_attr( $tip ); ?>">
     1136                        <span class="seomedic-mover-query">
     1137                                <?php if ( $edit ) : ?>
     1138                                        <a href="<?php echo esc_url( $edit ); ?>"><?php echo esc_html( $keyword ); ?></a>
     1139                                <?php else : ?>
     1140                                        <?php echo esc_html( $keyword ); ?>
     1141                                <?php endif; ?>
     1142                        </span>
     1143                        <span class="seomedic-mover-spark"><?php echo self::position_sparkline_svg( $points, $color ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_kses'd inside the helper. ?></span>
     1144                        <span class="seomedic-mover-pos"><?php echo esc_html( $dropped ? '—' : '#' . number_format_i18n( round( (float) $r['position'] ) ) ); ?></span>
     1145                        <span class="seomedic-mover-pill seomedic-mover-<?php echo esc_attr( $dir ); ?>"><?php echo esc_html( $pill ); ?></span>
     1146                </div>
     1147                <?php
     1148        }
     1149
    9981150        private static function render_movers( $movers ) {
    9991151                $gainers  = isset( $movers['gainers'] )  ? $movers['gainers']  : array();
  • seomedic-seo-link-auditor/trunk/includes/class-seo-integration.php

    r3791215 r3798783  
    215215                // as updated whenever we made it to the meta-update line above.
    216216                $changes['updated'] = true;
     217                // The dashboard widget's focus-keyword list caches post → keyword for an
     218                // hour (SEOMedic_GSC_Integration::TRANSIENT_FOCUS_KEYWORDS, Pro). Named
     219                // as a string so this core file never touches the Pro class.
     220                delete_transient( 'seomedic_focus_kw_posts' );
    217221                return $changes;
    218222        }
  • seomedic-seo-link-auditor/trunk/readme.txt

    r3798411 r3798783  
    55Requires at least: 5.8
    66Tested up to: 7.1
    7 Stable tag: 3.1.6
     7Stable tag: 3.1.7
    88Requires PHP: 7.4
    99License: GPLv2 or later
    … …  
    206206with the plugin in changelog.txt.
    207207
     208= 3.1.7 =
     209* New (Pro): "Your Focus Keywords" on the WordPress dashboard widget shows each post's focus keyword with its Google position, a trend line and the change over time, using history SEOMedic already records daily (no extra Google requests). Keywords without a position yet are counted, never given a made-up number.
     210
    208211= 3.1.6 =
    209212* Improved (Pro): the dashboard widget's Top 10 pages by clicks and Just Started Ranking now load by themselves a moment after the WordPress dashboard opens, instead of asking you to press Sync.
    … …  
    214217* New (Pro): the editor sidebar shows whether Google has indexed the post, its clicks and impressions, and the top searches it's found for, as soon as you open it (no Analyze needed), with a Check button. For an old post Google never indexed (or looked at and declined), it suggests redirecting it and lets you pick the post to redirect to right there.
    215218
    216 = 3.1.4 =
    217 * Improved: the Redirects list explains what each fix changes, and flags old archive pages (like /category/news/page/4) that redirect to a single post, with a one-click option to point them at the archive instead.
    218 
    219219== Upgrade Notice ==
     220
     221= 3.1.7 =
     222Your Focus Keywords on the dashboard widget: each post's focus keyword with its Google position and trend.
    220223
    221224= 3.1.6 =
  • seomedic-seo-link-auditor/trunk/seomedic.php

    r3798411 r3798783  
    44 * Plugin URI: https://getseomedic.com/
    55 * Description: Audit broken links, fix missing meta descriptions, detect H1 issues, repair YouTube embeds, audit Schema.org markup, and clean up internal linking — all from one dashboard.
    6  * Version: 3.1.6
     6 * Version: 3.1.7
    77 * Author: Andrew Murray
    88 * Author URI: https://getseomedic.com
    … …  
    174174                delete_transient( 'seomedic_dataforseo_balance' );
    175175                delete_transient( 'seomedic_serp_last_raw' );
     176                delete_transient( 'seomedic_focus_kw_posts' );
    176177                delete_option( 'seomedic_seo_tasklist' );
    177178                delete_metadata( 'user', 0, 'seomedic_review_notice', '', true );
    … …  
    192193 */
    193194if ( ! defined( 'SEOMEDIC_VERSION' ) ) {
    194         define( 'SEOMEDIC_VERSION', '3.1.6' );
     195        define( 'SEOMEDIC_VERSION', '3.1.7' );
    195196        define( 'SEOMEDIC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    196197        define( 'SEOMEDIC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
  • seomedic-seo-link-auditor/trunk/uninstall.php

    r3791215 r3798783  
    119119delete_transient( 'seomedic_mixed_content_audit' );
    120120delete_transient( 'seomedic_post_index' );
     121delete_transient( 'seomedic_focus_kw_posts' );
    121122
    122123// Per-user dismissal meta for the upgrade + review-request notices.
Note: See TracChangeset for help on using the changeset viewer.