Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to the Rivian Tire Guide plugin will be documented in this f

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [1.24.0] - 2026-03-02

### Added
- **Affiliate link health checker** — New `RTG_Link_Checker` class that detects broken affiliate links by following redirects and flagging links that land on the supplier homepage instead of the product page. Also catches HTTP errors (4xx/5xx) and connection failures.
- **Weekly automated checks** — WP-Cron runs the link health check once per week. A custom `weekly` cron schedule is registered for this purpose.
- **"Check Links Now" button** — Manual trigger on the Affiliate Links admin page to run the health check on demand without waiting for the weekly schedule.
- **Broken link badges** — Each broken tire row in the Affiliate Links table shows a red "Broken" badge with a tooltip explaining the failure reason (e.g. "Redirects to homepage").
- **"Broken" filter tab** — New filter tab on the Affiliate Links page to show only tires with broken links, alongside the existing All/Affiliate/Regular/Missing/No Review tabs.
- **Broken Links stat card** — New stat card in the Affiliate Links stats grid showing the count of broken links detected.
- **Broken link email notification** — HTML email sent to the site admin when broken links are found, listing each affected tire with its status and failure details, plus a direct link to the admin dashboard.
- **Dashboard health indicator** — Content Health section on the main dashboard now includes a broken affiliate links indicator with a link to the Affiliate Links page for remediation.

## [1.23.0] - 2026-03-01

### Added
Expand Down
100 changes: 99 additions & 1 deletion admin/views/affiliate-links.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ function rtg_truncate_url( $url ) {
}
}

// Link health check results.
$link_check_results = RTG_Link_Checker::get_results();
$broken_tire_ids = RTG_Link_Checker::get_broken_tire_ids();
$broken_count = count( $broken_tire_ids );
$last_checked = ! empty( $link_check_results['checked_at'] ) ? $link_check_results['checked_at'] : '';

// Success message.
$message = isset( $_GET['message'] ) ? sanitize_text_field( $_GET['message'] ) : '';
?>
Expand Down Expand Up @@ -113,6 +119,30 @@ function rtg_truncate_url( $url ) {
<div class="rtg-stat-value"><?php echo esc_html( $counts['has_review'] ); ?></div>
<div class="rtg-stat-label">Review Links</div>
</div>
<div class="rtg-stat-card">
<div class="rtg-stat-value" style="color: <?php echo $broken_count > 0 ? 'var(--rtg-error)' : 'var(--rtg-success)'; ?>;"><?php echo esc_html( $broken_count ); ?></div>
<div class="rtg-stat-label">Broken Links</div>
</div>
</div>

<!-- Link Health Check -->
<div class="rtg-card" style="margin-bottom:20px;">
<div class="rtg-card-body" style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;justify-content:space-between;">
<div>
<strong>Link Health Check</strong>
<?php if ( $last_checked ) : ?>
<span style="color:var(--rtg-text-muted);margin-left:8px;">Last checked: <?php echo esc_html( date( 'M j, Y g:i A', strtotime( $last_checked ) ) ); ?></span>
<?php else : ?>
<span style="color:var(--rtg-text-muted);margin-left:8px;">Never checked</span>
<?php endif; ?>
<?php if ( $broken_count > 0 ) : ?>
<span style="color:var(--rtg-error);margin-left:8px;font-weight:600;"><?php echo esc_html( $broken_count ); ?> broken <?php echo $broken_count === 1 ? 'link' : 'links'; ?> found</span>
<?php elseif ( $last_checked ) : ?>
<span style="color:var(--rtg-success);margin-left:8px;font-weight:600;">All links healthy</span>
<?php endif; ?>
</div>
<button type="button" id="rtg-check-links-btn" class="rtg-btn rtg-btn-secondary">Check Links Now</button>
</div>
</div>

<!-- Filter Tabs + Search -->
Expand All @@ -123,6 +153,7 @@ function rtg_truncate_url( $url ) {
'affiliate' => 'Affiliate (' . $total_affiliate . ')',
'regular' => 'Regular (' . $total_regular . ')',
'missing' => 'Missing Link (' . $total_missing . ')',
'broken' => 'Broken (' . $broken_count . ')',
'no_review' => 'No Review (' . $counts['missing_review'] . ')',
);
foreach ( $tabs as $key => $label ) :
Expand Down Expand Up @@ -186,14 +217,24 @@ function rtg_truncate_url( $url ) {
$badge_class = 'rtg-badge-warning';
$badge_label = 'Regular';
}
$is_broken = isset( $broken_tire_ids[ $tire['tire_id'] ] );
$broken_reason = $is_broken ? $broken_tire_ids[ $tire['tire_id'] ] : '';

// Skip non-broken tires when "broken" filter is active.
if ( $link_filter === 'broken' && ! $is_broken ) {
continue;
}
?>
<tr data-tire-id="<?php echo esc_attr( $tire['tire_id'] ); ?>">
<tr data-tire-id="<?php echo esc_attr( $tire['tire_id'] ); ?>"<?php echo $is_broken ? ' data-broken="1"' : ''; ?>>
<td>
<strong><?php echo esc_html( $tire['brand'] . ' ' . $tire['model'] ); ?></strong>
<div style="font-size:12px;color:var(--rtg-text-muted);"><?php echo esc_html( $tire['tire_id'] ); ?> &middot; <?php echo esc_html( $tire['size'] ); ?> &middot; <?php echo esc_html( $tire['category'] ); ?></div>
</td>
<td>
<span class="rtg-badge <?php echo esc_attr( $badge_class ); ?> rtg-link-status-badge"><?php echo esc_html( $badge_label ); ?></span>
<?php if ( $is_broken ) : ?>
<span class="rtg-badge rtg-badge-error rtg-broken-badge" title="<?php echo esc_attr( $broken_reason ); ?>" style="margin-top:4px;display:inline-block;background:#ef4444;color:#fff;font-size:11px;">Broken</span>
<?php endif; ?>
</td>
<td class="rtg-link-cell" data-field="link">
<div class="rtg-link-display">
Expand Down Expand Up @@ -362,6 +403,63 @@ function escHtml(str) {
return div.innerHTML;
}

// Check Links Now button.
$(document).on('click', '#rtg-check-links-btn', function() {
var $btn = $(this);
$btn.prop('disabled', true).text('Checking...');

$.post(ajaxUrl, {
action: 'rtg_check_links',
nonce: nonce
}, function(response) {
$btn.prop('disabled', false).text('Check Links Now');

if (response.success) {
var data = response.data;
var brokenCount = (data.broken || []).length;

if (brokenCount > 0) {
// Mark broken rows.
var brokenIds = {};
for (var i = 0; i < data.broken.length; i++) {
brokenIds[data.broken[i].tire_id] = data.broken[i].reason;
}

$('tr[data-tire-id]').each(function() {
var $row = $(this);
var tireId = $row.data('tire-id');
var $statusCell = $row.find('td:eq(1)');

// Remove any existing broken badge.
$statusCell.find('.rtg-broken-badge').remove();

if (brokenIds[tireId]) {
$row.attr('data-broken', '1');
$statusCell.append('<span class="rtg-badge rtg-badge-error rtg-broken-badge" title="' + escHtml(brokenIds[tireId]) + '" style="margin-top:4px;display:inline-block;background:#ef4444;color:#fff;font-size:11px;">Broken</span>');
} else {
$row.removeAttr('data-broken');
}
});

alert('Check complete: ' + brokenCount + ' broken link' + (brokenCount !== 1 ? 's' : '') + ' found. An email notification has been sent.');
} else {
// Remove all broken badges.
$('tr[data-tire-id]').removeAttr('data-broken');
$('.rtg-broken-badge').remove();
alert('Check complete: All ' + data.total + ' links are healthy!');
}

// Reload page to update counts.
location.reload();
} else {
alert('Error: ' + (response.data || 'Link check failed.'));
}
}).fail(function() {
$btn.prop('disabled', false).text('Check Links Now');
alert('Network error. Please try again.');
});
});

// Dismiss notices.
$(document).on('click', '.rtg-notice-dismiss', function() {
$(this).closest('.rtg-notice').fadeOut(200, function() { $(this).remove(); });
Expand Down
30 changes: 30 additions & 0 deletions admin/views/dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
// Pending reviews.
$pending_reviews = (int) ( $stats['pending_reviews'] ?? 0 );

// Broken affiliate links.
$broken_links = RTG_Link_Checker::get_broken_tire_ids();
$broken_link_count = count( $broken_links );
$link_check_results = RTG_Link_Checker::get_results();
$last_link_check = ! empty( $link_check_results['checked_at'] ) ? $link_check_results['checked_at'] : '';

// Helper: find max count in a grouped result for bar widths.
$max_of = function ( $rows ) {
$max = 0;
Expand Down Expand Up @@ -339,6 +345,30 @@
</span>
<?php endif; ?>
</div>

<!-- Broken Affiliate Links -->
<div class="rtg-health-item">
<span class="rtg-health-icon <?php echo $broken_link_count > 0 ? 'rtg-health-icon-error' : 'rtg-health-icon-success'; ?>">
<span class="dashicons <?php echo $broken_link_count > 0 ? 'dashicons-warning' : 'dashicons-yes-alt'; ?>"></span>
</span>
<span class="rtg-health-content">
<strong><?php echo esc_html( $broken_link_count ); ?> Broken Affiliate <?php echo $broken_link_count !== 1 ? 'Links' : 'Link'; ?></strong>
<p><?php
if ( ! $last_link_check ) {
echo 'No link check has run yet.';
} elseif ( $broken_link_count > 0 ) {
echo 'Links redirecting to homepage instead of product page.';
} else {
echo 'All affiliate links are working correctly.';
}
?></p>
</span>
<?php if ( $broken_link_count > 0 ) : ?>
<span class="rtg-health-action">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=rtg-affiliate-links&link_filter=broken' ) ); ?>" class="rtg-btn rtg-btn-secondary" style="text-decoration:none;">Fix Links</a>
</span>
<?php endif; ?>
</div>
</div>
</div>

Expand Down
21 changes: 21 additions & 0 deletions includes/class-rtg-ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public function __construct() {
// Efficiency calculator — admin only.
add_action( 'wp_ajax_rtg_calculate_efficiency', array( $this, 'calculate_efficiency' ) );

// Link health check — admin only.
add_action( 'wp_ajax_rtg_check_links', array( $this, 'check_links' ) );

// AI tire recommendations — public.
add_action( 'wp_ajax_rtg_ai_recommend', array( $this, 'ai_recommend' ) );
add_action( 'wp_ajax_nopriv_rtg_ai_recommend', array( $this, 'ai_recommend' ) );
Expand Down Expand Up @@ -800,6 +803,24 @@ public function ai_recommend() {
wp_send_json_success( $result );
}

/**
* Run the affiliate link health check on demand.
* Requires manage_options capability.
*/
public function check_links() {
if ( ! check_ajax_referer( 'rtg_affiliate_links_nonce', 'nonce', false ) ) {
wp_send_json_error( 'Security check failed.' );
}

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( 'Unauthorized.' );
}

$results = RTG_Link_Checker::run();

wp_send_json_success( $results );
}

/**
* Get analytics data for the admin dashboard.
* Requires manage_options capability.
Expand Down
1 change: 1 addition & 0 deletions includes/class-rtg-deactivator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public static function deactivate() {
flush_rewrite_rules();
delete_option( 'rtg_flush_rewrite' );
wp_clear_scheduled_hook( 'rtg_analytics_cleanup' );
RTG_Link_Checker::unschedule();
}
}
Loading
Loading