Skip to main content

Overview

The 404 Monitor tracks broken links on your site by logging 404 (Page Not Found) errors with detailed information including URLs, referrers, IP addresses, and timestamps.
Identifying and fixing 404 errors improves user experience, SEO rankings, and helps discover opportunities for redirects.

Features

Automatic Logging

Captures 404 errors as they occur in real-time

Referrer Tracking

See where broken links are coming from

Retention Controls

Automatically delete old logs after specified days

Privacy-Focused

Opt-in logging with configurable data retention

Configuration

1

Access 404 Settings

Navigate to Settings → GEO AI → Redirects & 404
2

Enable Logging

Check “Log 404 errors” to start tracking
3

Set Retention Period

Enter number of days to keep logs (default: 30 days)
4

Set Max Entries

Configure maximum log entries (default: 1,000)
5

Save Changes

Click Save Changes to activate monitoring
404 logging is opt-in and disabled by default for privacy and performance reasons.

Implementation Details

404 Logging Class

includes/class-geoai-404.php
class GeoAI_404 {
    private static $instance = null;
    private $table_name;

    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        global $wpdb;
        $this->table_name = $wpdb->prefix . 'geoai_404_log';

        add_action( 'template_redirect', array( $this, 'log_404' ) );
    }
}

Logging Function

includes/class-geoai-404.php
public function log_404() {
    if ( ! is_404() ) {
        return;
    }

    $settings = get_option( 'geoai_404_settings', array() );
    
    if ( empty( $settings['enabled'] ) ) {
        return;
    }

    global $wpdb;

    $url      = $_SERVER['REQUEST_URI'];
    $referrer = $_SERVER['HTTP_REFERER'] ?? '';
    $ip       = $this->get_client_ip();

    $wpdb->insert(
        $this->table_name,
        array(
            'url'       => $url,
            'referrer'  => $referrer,
            'ip'        => $ip,
            'timestamp' => current_time( 'mysql' ),
        ),
        array( '%s', '%s', '%s', '%s' )
    );

    $this->cleanup_old_logs();
}

IP Detection

includes/class-geoai-404.php
private function get_client_ip() {
    $ip = '';
    
    if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
        $ip = $_SERVER['HTTP_CLIENT_IP'];
    } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    } else {
        $ip = $_SERVER['REMOTE_ADDR'];
    }

    return sanitize_text_field( $ip );
}

Auto-Cleanup

includes/class-geoai-404.php
private function cleanup_old_logs() {
    global $wpdb;

    $settings = get_option( 'geoai_404_settings', array() );
    $retention = $settings['retention'] ?? 30;

    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM {$this->table_name} WHERE timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)",
            $retention
        )
    );
}

Database Schema

The 404 log uses a dedicated database table:
CREATE TABLE wp_geoai_404_log (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  url varchar(255) NOT NULL,
  referrer varchar(255) DEFAULT NULL,
  ip varchar(45) DEFAULT NULL,
  timestamp datetime NOT NULL,
  PRIMARY KEY (id),
  KEY timestamp (timestamp),
  KEY url (url)
);
Columns:
  • id: Unique identifier for each log entry
  • url: The requested URL that returned 404
  • referrer: The page that linked to the broken URL
  • ip: IP address of the visitor
  • timestamp: When the 404 occurred

Viewing 404 Logs

Recent errors appear at the bottom of the Redirects & 404 settings page:
includes/class-geoai-admin.php
private function render_404_log_viewer() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'geoai_404_log';
    
    // Check if table exists
    if ( $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) !== $table_name ) {
        return;
    }

    $logs = $wpdb->get_results(
        "SELECT * FROM {$table_name} ORDER BY timestamp DESC LIMIT 50",
        ARRAY_A
    );

    if ( empty( $logs ) ) {
        return;
    }
    ?>
    <h3><?php esc_html_e( 'Recent 404 Errors', 'geo-ai' ); ?></h3>
    <table class="widefat">
        <thead>
            <tr>
                <th><?php esc_html_e( 'URL', 'geo-ai' ); ?></th>
                <th><?php esc_html_e( 'Referrer', 'geo-ai' ); ?></th>
                <th><?php esc_html_e( 'IP', 'geo-ai' ); ?></th>
                <th><?php esc_html_e( 'Date', 'geo-ai' ); ?></th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ( $logs as $log ) : ?>
            <tr>
                <td><code><?php echo esc_html( $log['url'] ); ?></code></td>
                <td><?php echo esc_html( $log['referrer'] ? $log['referrer'] : '-' ); ?></td>
                <td><?php echo esc_html( $log['ip'] ); ?></td>
                <td><?php echo esc_html( $log['timestamp'] ); ?></td>
            </tr>
            <?php endforeach; ?>
        </tbody>
    </table>
    <?php
}

Log Display Example

URLReferrerIPDate
/old-producthttps://google.com/192.168.1.12025-03-04 15:30:22
/blog/missing-posthttps://example.com/blog/192.168.1.22025-03-04 14:15:10
/contact-old-192.168.1.32025-03-04 12:45:00

Common 404 Patterns

Indicators:
  • No referrer or search engine referrer
  • Old URLs that used to exist
Action:
  • Create redirects to relevant current content
  • Use Google Search Console to see which URLs Google expects
Indicators:
  • URLs with obvious typos
  • Random URLs not related to your content
Action:
  • Usually safe to ignore
  • Consider creating redirect only if pattern is frequent
Indicators:
  • Requests for /wp-admin, /xmlrpc.php, .env, etc.
  • Many requests from same IP
Action:
  • Safe to ignore (security plugin should handle)
  • Consider IP blocking if excessive

Analyzing 404 Data

Most Common 404s

Query the database to find frequently broken URLs:
global $wpdb;
$table = $wpdb->prefix . 'geoai_404_log';

$common_404s = $wpdb->get_results("
    SELECT url, COUNT(*) as hits
    FROM {$table}
    WHERE timestamp > DATE_SUB(NOW(), INTERVAL 30 DAY)
    GROUP BY url
    ORDER BY hits DESC
    LIMIT 20
");

foreach ( $common_404s as $error ) {
    echo $error->url . ': ' . $error->hits . ' hits<br>';
}

404s by Referrer

Find which pages are linking to broken URLs:
$by_referrer = $wpdb->get_results("
    SELECT referrer, COUNT(*) as count
    FROM {$table}
    WHERE referrer != ''
    GROUP BY referrer
    ORDER BY count DESC
    LIMIT 10
");

Creating Redirects from 404s

1

Review 404 Log

Check the Recent 404 Errors table for broken URLs
2

Identify Redirect Target

Determine where the old URL should redirect to
3

Add Redirect

Scroll to the Redirects section and click “Add Redirect”
4

Enter Details

  • From: Copy the 404 URL (e.g., /old-product)
  • To: Enter destination (e.g., /products/new-product)
  • Type: Select 301 Permanent
5

Save & Test

Save changes and test the old URL to verify redirect works

Retention & Privacy

Data Retention

Retention Period

Default: 30 daysAutomatically deletes logs older than specified days

Max Entries

Default: 1,000 entriesPrevents database bloat on high-traffic sites

Privacy Considerations

404 logs contain visitor IP addresses. Ensure compliance with GDPR, CCPA, and other privacy regulations.
Best practices:
  • Enable logging only when needed
  • Set conservative retention periods (7-30 days)
  • Disclose logging in your privacy policy
  • Don’t store logs longer than necessary
  • Consider anonymizing or hashing IPs

IP Anonymization

Anonymize IP addresses for better privacy:
add_filter( 'geoai_404_log_ip', 'anonymize_ip_address' );

function anonymize_ip_address( $ip ) {
    // Remove last octet from IPv4
    if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
        return preg_replace( '/\d+$/', '0', $ip );
    }
    
    // Remove last 80 bits from IPv6
    if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
        return substr( $ip, 0, strrpos( $ip, ':' ) ) . ':0000';
    }
    
    return $ip;
}

Performance Impact

Low Overhead

Single database INSERT per 404 (typically less than 5ms)

Minimal Queries

No queries on successful page loads, only on 404s

Auto-Cleanup

Old logs deleted automatically to prevent bloat

Optional Feature

Can be disabled completely if not needed

Export 404 Data

Export logs for analysis in external tools:
global $wpdb;
$table = $wpdb->prefix . 'geoai_404_log';

$logs = $wpdb->get_results(
    "SELECT * FROM {$table} ORDER BY timestamp DESC",
    ARRAY_A
);

// Output CSV
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="404-logs.csv"');

$output = fopen('php://output', 'w');
fputcsv($output, array('URL', 'Referrer', 'IP', 'Timestamp'));

foreach ($logs as $log) {
    fputcsv($output, [
        $log['url'],
        $log['referrer'],
        $log['ip'],
        $log['timestamp'],
    ]);
}

fclose($output);
exit;

Best Practices

Review Regularly

Check 404 logs weekly to catch new broken links quickly.

Fix Internal Links

Update broken internal links in your content rather than relying on redirects.

Redirect High-Value Pages

Create 301 redirects for URLs with backlinks or traffic.

Ignore Bot Scans

Don’t create redirects for obvious scanning attempts (.php, .env, etc.).

Set Conservative Retention

Keep logs for 7-30 days max to comply with privacy regulations.

Monitor After Migrations

Enable logging during site restructures to catch missed redirects.

Troubleshooting

Check:
  • Logging is enabled in Settings → Redirects & 404
  • Database table exists (check during plugin activation)
  • You’re viewing actual 404 pages (not redirects)
  • No caching plugin preventing 404 detection
Solution:
  • Deactivate and reactivate GEO AI plugin
  • Or run table creation manually:
global $wpdb;
$table = $wpdb->prefix . 'geoai_404_log';

$sql = "CREATE TABLE {$table} (
    id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    url varchar(255) NOT NULL,
    referrer varchar(255) DEFAULT NULL,
    ip varchar(45) DEFAULT NULL,
    timestamp datetime NOT NULL,
    PRIMARY KEY (id),
    KEY timestamp (timestamp),
    KEY url (url)
) {$wpdb->get_charset_collate()};";

require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
Solution:
  • Lower retention period (e.g., 7 days)
  • Lower max entries (e.g., 500)
  • Manually delete old logs:
global $wpdb;
$table = $wpdb->prefix . 'geoai_404_log';
$wpdb->query("TRUNCATE TABLE {$table}");
Cause: Very high 404 rate (bot attacks, missing files)Solution:
  • Disable logging temporarily
  • Add database index on url column
  • Increase auto-cleanup frequency
  • Block malicious IPs at server level

Advanced Usage

Custom 404 Page

Show most common 404s on your custom 404 page:
404.php
<?php
get_header();

global $wpdb;
$table = $wpdb->prefix . 'geoai_404_log';

// Get top 5 requested pages
$popular = $wpdb->get_results("
    SELECT url, COUNT(*) as hits
    FROM {$table}
    GROUP BY url
    ORDER BY hits DESC
    LIMIT 5
", ARRAY_A);
?>

<h1>Page Not Found</h1>
<p>Sorry, the page you're looking for doesn't exist.</p>

<h2>Popular Pages</h2>
<ul>
<?php foreach ($popular as $page) : ?>
    <li><a href="<?php echo esc_url($page['url']); ?>">
        <?php echo esc_html($page['url']); ?>
    </a></li>
<?php endforeach; ?>
</ul>

<?php get_footer(); ?>

Redirects

Create redirects for broken URLs

Sitemaps

Help search engines find valid URLs

Canonical URLs

Prevent duplicate content issues

Build docs developers (and LLMs) love