';
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin view.
$post_id = absint( wp_unslash( (string) ( $_GET['forwp_post'] ?? '' ) ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
?>
0 ) {
self::render_detail( $post_id );
} else {
self::render_list();
}
?>
Menu::DYNAMICS_PAGE_SLUG,
'forwp_post' => absint( $post_id ),
],
admin_url( 'admin.php' )
);
}
public static function list_url(): string {
return admin_url( 'admin.php?page=' . Menu::DYNAMICS_PAGE_SLUG );
}
private static function render_list(): void {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only list filters.
$paged = max( 1, absint( wp_unslash( (string) ( $_GET['paged'] ?? '1' ) ) ) );
$search = sanitize_text_field( wp_unslash( (string) ( $_GET['s'] ?? '' ) ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$per_page = 20;
$result = History::listed_posts( $paged, $per_page, $search );
$items = $result['items'];
$total = $result['total'];
$pages = max( 1, (int) ceil( $total / $per_page ) );
$waiting = History::waiting_for_crawl( 8 );
$ids = array_map( static fn( array $row ): int => (int) $row['post_id'], $items );
$typed = [];
foreach ( array_unique( array_filter( array_column( $items, 'last_type' ) ) ) as $type ) {
$typed[ (string) $type ] = History::latest_typed( $ids, (string) $type, 2 );
}
?>
%2$s',
esc_url( self::url_for_post( (int) $row['post_id'] ) ),
esc_html( self::post_label( (int) $row['post_id'] ) )
);
}
echo wp_kses_post( implode( ', ', $wait_links ) );
?>
';
echo '' . esc_html(
sprintf(
/* translators: %s: number of pages */
_n( '%s page', '%s pages', $total, '4wp-seo-helper' ),
number_format_i18n( $total )
)
) . '';
if ( $pages > 1 ) {
$base = add_query_arg(
array_filter(
[
'page' => Menu::DYNAMICS_PAGE_SLUG,
's' => $search,
]
),
admin_url( 'admin.php' )
);
echo '';
}
echo '
';
?>
' . esc_html__( 'Post not found.', '4wp-seo-helper' ) . '
';
return;
}
$events = History::for_post( $post_id );
if ( [] === $events ) {
HistoryLogger::seed_existing( $post );
$events = History::for_post( $post_id );
}
$latest = History::latest_by_type( $post_id );
$inspect = Indexing::inventory_fields( $post_id );
$chars = HistoryLogger::character_count( $post );
$edit = get_edit_post_link( $post_id, 'raw' );
?>
←
·
·
·
0
? gmdate( 'Y-m-d H:i:s', $inspect['gsc_index_requested_at'] )
: ''
);
self::render_check_item(
__( 'Robot crawled', '4wp-seo-helper' ),
$latest[ History::TYPE_CRAWL ],
(string) $inspect['gsc_last_crawl']
);
self::render_check_item(
__( 'Content length', '4wp-seo-helper' ),
$latest[ History::TYPE_CONTENT ],
'',
sprintf(
/* translators: %s: character count */
__( 'Now: %s characters', '4wp-seo-helper' ),
number_format_i18n( $chars )
)
);
self::render_check_item(
__( 'SEO fields', '4wp-seo-helper' ),
$latest[ History::TYPE_SEO ]
);
self::render_check_item(
__( 'Search metrics', '4wp-seo-helper' ),
$latest[ History::TYPE_GSC ]
);
?>
|null $event
*/
private static function render_check_item( string $label, ?array $event, string $fallback_date = '', string $extra = '' ): void {
$done = null !== $event || '' !== $fallback_date;
$when = '';
if ( is_array( $event ) ) {
$when = self::format_gmt( (string) $event['occurred_at'] );
} elseif ( '' !== $fallback_date ) {
$when = self::format_gmt( $fallback_date );
}
?>
$row
* @param array|null $previous
*/
private static function render_event_item( array $row, ?array $previous = null ): void {
$type = (string) $row['event_type'];
$snap = (array) ( $row['snapshot'] ?? [] );
$delta = '';
if ( is_array( $previous ) ) {
$delta = self::snapshot_delta( (array) ( $previous['snapshot'] ?? [] ), $snap, $type );
}
?>
$snapshot
*/
private static function snapshot_summary( array $snapshot ): string {
$parts = [];
if ( isset( $snapshot['chars'] ) ) {
$parts[] = sprintf(
/* translators: %s: character count */
__( '%s chars', '4wp-seo-helper' ),
number_format_i18n( (int) $snapshot['chars'] )
);
}
if ( isset( $snapshot['score'] ) ) {
$parts[] = sprintf(
/* translators: %d: SEO score */
__( 'score %d', '4wp-seo-helper' ),
(int) $snapshot['score']
);
}
if ( ! empty( $snapshot['keys'] ) && is_array( $snapshot['keys'] ) ) {
$parts[] = implode( ', ', array_map( 'strval', array_slice( $snapshot['keys'], 0, 5 ) ) );
}
if ( isset( $snapshot['clicks'] ) || isset( $snapshot['impr'] ) ) {
$parts[] = sprintf(
/* translators: 1: clicks, 2: impressions, 3: average position */
__( '%1$s clicks / %2$s impr / pos %3$s', '4wp-seo-helper' ),
number_format_i18n( (int) ( $snapshot['clicks'] ?? 0 ) ),
number_format_i18n( (int) ( $snapshot['impr'] ?? 0 ) ),
isset( $snapshot['pos'] ) ? (string) $snapshot['pos'] : '—'
);
}
if ( ! empty( $snapshot['sugg'] ) && is_array( $snapshot['sugg'] ) ) {
$queries = [];
foreach ( array_slice( $snapshot['sugg'], 0, 3 ) as $row ) {
if ( is_array( $row ) && ! empty( $row['q'] ) ) {
$queries[] = (string) $row['q'];
}
}
if ( [] !== $queries ) {
$parts[] = sprintf(
/* translators: %s: comma-separated search queries */
__( 'suggest: %s', '4wp-seo-helper' ),
implode( ', ', $queries )
);
}
}
if ( ! empty( $snapshot['cov'] ) ) {
$parts[] = (string) $snapshot['cov'];
}
return implode( ' · ', $parts );
}
/**
* @param array $from
* @param array $to
*/
private static function snapshot_delta( array $from, array $to, string $type ): string {
if ( History::TYPE_CONTENT === $type && isset( $from['chars'], $to['chars'] ) && (int) $from['chars'] !== (int) $to['chars'] ) {
return sprintf(
/* translators: 1: previous character count, 2: new character count */
__( '%1$s → %2$s characters', '4wp-seo-helper' ),
number_format_i18n( (int) $from['chars'] ),
number_format_i18n( (int) $to['chars'] )
);
}
if ( History::TYPE_GSC === $type && ( isset( $from['clicks'] ) || isset( $to['clicks'] ) ) ) {
return sprintf(
/* translators: 1: previous clicks, 2: new clicks, 3: previous position, 4: new position */
__( 'clicks %1$s → %2$s · pos %3$s → %4$s', '4wp-seo-helper' ),
number_format_i18n( (int) ( $from['clicks'] ?? 0 ) ),
number_format_i18n( (int) ( $to['clicks'] ?? 0 ) ),
isset( $from['pos'] ) ? (string) $from['pos'] : '—',
isset( $to['pos'] ) ? (string) $to['pos'] : '—'
);
}
if ( History::TYPE_CRAWL === $type && ! empty( $from['crawl'] ) && ! empty( $to['crawl'] ) && $from['crawl'] !== $to['crawl'] ) {
return sprintf(
/* translators: 1: previous crawl datetime, 2: new crawl datetime */
__( 'previous crawl %1$s → %2$s', '4wp-seo-helper' ),
self::format_gmt( (string) $from['crawl'] ),
self::format_gmt( (string) $to['crawl'] )
);
}
return '';
}
/**
* @param array $current Latest snapshot.
* @param array|null $previous Previous snapshot of the same type.
*/
private static function change_summary( string $type, array $current, ?array $previous ): string {
if ( History::TYPE_GSC === $type ) {
$clicks = (int) ( $current['clicks'] ?? 0 );
$impr = (int) ( $current['impr'] ?? 0 );
if ( is_array( $previous ) ) {
$dc = $clicks - (int) ( $previous['clicks'] ?? 0 );
$di = $impr - (int) ( $previous['impr'] ?? 0 );
$parts = [];
if ( 0 !== $dc ) {
$parts[] = sprintf( '%+d clicks', $dc );
}
if ( 0 !== $di ) {
$parts[] = sprintf( '%+d impr', $di );
}
if ( [] !== $parts ) {
return implode( ' · ', $parts );
}
}
return sprintf(
/* translators: 1: clicks, 2: impressions */
__( '%1$s clicks · %2$s impr', '4wp-seo-helper' ),
number_format_i18n( $clicks ),
number_format_i18n( $impr )
);
}
if ( History::TYPE_CONTENT === $type ) {
$chars = (int) ( $current['chars'] ?? 0 );
if ( is_array( $previous ) && (int) ( $previous['chars'] ?? 0 ) !== $chars ) {
return sprintf(
/* translators: 1: previous character count, 2: new character count */
__( '%1$s → %2$s characters', '4wp-seo-helper' ),
number_format_i18n( (int) $previous['chars'] ),
number_format_i18n( $chars )
);
}
return sprintf(
/* translators: %s: character count */
__( '%s characters', '4wp-seo-helper' ),
number_format_i18n( $chars )
);
}
if ( History::TYPE_SEO === $type ) {
$score = (int) ( $current['score'] ?? 0 );
if ( is_array( $previous ) && (int) ( $previous['score'] ?? 0 ) !== $score ) {
return sprintf(
/* translators: 1: previous score, 2: new score */
__( 'Score %1$d → %2$d', '4wp-seo-helper' ),
(int) $previous['score'],
$score
);
}
return sprintf(
/* translators: %d: SEO score */
__( 'Score %d', '4wp-seo-helper' ),
$score
);
}
if ( History::TYPE_CRAWL === $type ) {
return __( 'Google crawled this URL', '4wp-seo-helper' );
}
if ( History::TYPE_INDEX_REQUEST === $type ) {
return __( 'Indexing requested', '4wp-seo-helper' );
}
if ( History::TYPE_INSPECT === $type ) {
return __( 'URL inspected in Search Console', '4wp-seo-helper' );
}
return self::event_label( $type );
}
private static function event_label( string $type ): string {
$labels = [
History::TYPE_INDEX_REQUEST => __( 'Index request', '4wp-seo-helper' ),
History::TYPE_INSPECT => __( 'Inspect', '4wp-seo-helper' ),
History::TYPE_CRAWL => __( 'Crawl', '4wp-seo-helper' ),
History::TYPE_CONTENT => __( 'Content', '4wp-seo-helper' ),
History::TYPE_SEO => __( 'SEO', '4wp-seo-helper' ),
History::TYPE_GSC => __( 'Search', '4wp-seo-helper' ),
];
return $labels[ $type ] ?? ( '' !== $type ? $type : '—' );
}
private static function post_label( int $post_id ): string {
$title = get_the_title( $post_id );
return '' !== $title ? $title : '#' . $post_id;
}
private static function format_relative( string $gmt ): string {
$absolute = self::format_gmt( $gmt );
if ( '—' === $absolute ) {
return $absolute;
}
$ts = strtotime( $gmt . ' UTC' );
if ( false === $ts ) {
return $absolute;
}
return sprintf(
/* translators: %s: human-readable time difference */
__( '%s ago', '4wp-seo-helper' ),
human_time_diff( $ts )
);
}
private static function format_gmt( string $gmt ): string {
if ( '' === $gmt ) {
return '—';
}
$local = get_date_from_gmt( $gmt );
if ( ! is_string( $local ) || '' === $local || '0000-00-00 00:00:00' === $local ) {
$ts = strtotime( $gmt );
if ( false === $ts ) {
return $gmt;
}
$local = wp_date( 'Y-m-d H:i:s', $ts );
}
$formatted = mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $local, true );
return is_string( $formatted ) && '' !== $formatted ? $formatted : $gmt;
}
}