Funktion zum Auffinden und Korrigieren defekter Links in WordPress

22.08.2026|

Tiempo de lectura Lectura: 27 min, 30 s
Número de palabras Palabras: 5089
Número de visitas Visitas: 42
Icono de traducción

Defekte Links führen zu einem Verlust an Link-Autorität („Link Juice“) und verbrauchen dasCrawl-Budget, da die Bots der Suchmaschinen Zeit damit verschwenden, HTTP-404-Antworten zu indexieren, anstatt wertvolle Inhalte zu erfassen. Zudem verschlechtern sie die Benutzererfahrung, indem sie den Navigationsfluss unterbrechen und die Absprungrate erhöhen. Durch die Korrektur mittels 301-Weiterleitungen oder der Aktualisierung der URLs wird das Crawling der Website optimiert, der Fluss der Link-Autorität zu den Zielseiten erhalten und die Architektur der Website gestärkt, um indirekte Abstrafung im organischen Ranking zu vermeiden.

Bei der routinemäßigen Überprüfung auf defekte Links mit dem altbewährten Plugin „Broken Link Checker“ stellte ich fest, dass der lokale Scan nicht funktionierte. Also habe ich eine sehr einfache Funktion, die ich noch irgendwo herumliegen hatte, genommen und sie mit Hilfe von Gemini ein wenig verbessert, um sie benutzerfreundlicher zu gestalten und den Scan sowie die Korrektur der defekten Links durchzuführen.

Zwar schießen defekte Links wie Pilze aus dem Boden, doch auf dieser Reise ist mir ein erheblicher Rückgang bei den Twitter-Links (X) aufgefallen, wo die Accounts in Scharen verschwinden. Es tauchen bereits Hunderte von Accounts und/oder Tweets auf, die nicht mehr existieren, aber ich werde dieses Thema für einen zukünftigen Beitrag über den Tod des Internets, wie wir es kannten, aufheben.

Benutzeroberfläche der Funktion zur Überprüfung defekter Links für WordPress mit Erläuterungen zu den einzelnen Bestandteilen

Um die Funktion zu nutzen, füge das Snippet einfach in die Datei „functions.php“ deines Templates oder Child-Themes oder in dein Snippet-Plugin ein. Unter „Extras/Link-Inspektor“ findest du das Tool und seine Optionen, die im Folgenden näher beschrieben werden.

1 – Statusarten. Hier kannst du aus über 10 Statusarten für einen selektiven Scan wählen.

2 – Die Schaltfläche „Kontinuierlichen Scan starten“ führt einen Scan ohne Unterbrechungen durch. Auch wenn dies eigentlich nicht der Fall sein sollte, kann es dennoch gewisse Auswirkungen auf die Ladegeschwindigkeit der Website haben. Ich habe den Scan bereits stundenlang ohne Probleme laufen lassen.

3 – Stapelscan. Jedes Mal, wenn Sie diese Schaltfläche anklicken, werden 10 Einträge gescannt.

4 – JSON exportieren/importieren. Da das Plugin keine Daten in die Datenbank schreibt, geht die Liste verloren, wenn Sie den Browser schließen. Um alle Einträge wiederherzustellen, an die Sie sich nicht mehr erinnern können, müssen Sie Ihre Scan-Liste daher immer mit „Exportieren“ speichern. Um sie wiederherzustellen, müssen Sie sie lediglich importieren.

5 – Informationen zum Scanvorgang.

6 – Nach Typen filtern. Sobald der Scan abgeschlossen ist, kannst du die Listenansicht nach Status filtern.

7 – Beitrag/Seite. Hier findest du einen Link zum Bearbeiten des Beitrags und einen weiteren Link („Anzeigen“), um ihn aufzurufen.

8 – Der Anker des Links, damit du ihn schneller findest.

9 – Die URL des defekten Links.

10 – HTTP-Statuscode des Links.

11 – Funktion: Das Häkchen entfernt lediglich diese Zeile aus der Listenansicht. Es werden weder Änderungen vorgenommen noch Einträge gelöscht oder hinzugefügt.

12 – Ersatz-URL. Feld, in das der Ersatzlink eingegeben wird.

13 – Suche auf Archive.org. Wenn Sie hier klicken, öffnet sich die Wayback Machine mit einer Liste der verfügbaren Datumsangaben für die Kopien Ihrer URL (sofern diese noch vorhanden sind), sodass Sie diejenige auswählen können, die Sie am meisten interessiert.

14 – Link entfernen. Diese Option entfernt den Hyperlink, wobei der Linktext im Beitrag oder auf der Seite erhalten bleibt, falls du keine alternative URL hast oder keine hinzufügen möchtest.

Zunächst einmal sei gesagt, dass es sich um eine Einwegfunktion handelt; es wird empfohlen, sie zu entfernen, sobald du mit der Bereinigung fertig bist. Ich glaube nicht, dass ich sie noch weiter anpassen werde, aber sollte ich es doch tun, werde ich den Code hier aktualisieren.

Auch wenn der Code letztendlich für ein Snippet – das ja eigentlich kurz oder ein Ausschnitt sein soll – zu lang geworden ist, funktioniert er doch recht gut. Zwar lässt er sich noch verfeinern, um bestimmte 404-Falschmeldungen zu vermeiden, und durch die Mehrfachauswahl von Aktionen verbessern, aber wenn man nicht allzu viele Links hat und keine Zeit, diese manuell zu überprüfen, ist er voll funktionsfähig.

Für das massenhafte Ersetzen von Links kannst du auf die klassischen Plugins wie „Better Search Replace“ und „Search Regex“ zurückgreifen.

// Inspector de Enlaces Rotos (Con Reemplazo Directo AJAX, Desvinculación de Enlace, Filtro de Vista, Exportar/Importar JSON)

if (!defined('ABSPATH')) exit;

// 1. Menú en Herramientas
add_action('admin_menu', function() {
    add_management_page('Inspector Enlaces', 'Inspector Enlaces', 'manage_options', 'inspector-enlaces-rotos', 'ier_render_admin_page');
});

// 2. Registro AJAX
add_action('wp_ajax_ier_scan_single_post', 'ier_scan_single_post_callback');
add_action('wp_ajax_ier_replace_link', 'ier_replace_link_callback');
add_action('wp_ajax_ier_unlink_link', 'ier_unlink_link_callback');

// 3. Interfaz HTML + JavaScript
function ier_render_admin_page() {
    global $wpdb;
    $ajax_url = admin_url('admin-ajax.php');
    $total_posts = (int) $wpdb->get_var("SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type IN ('post', 'page') AND post_status = 'publish'");
    
    echo '<div class="wrap">';
    echo '<h1>Inspector de Enlaces Rotos</h1>';
    echo '<p>Escaneo ultra-ligero con reemplazo directo, desvinculación de enlaces, filtro por estados, exportación/importación JSON y auto-recuperación.</p>';
    
    echo '<div style="background: #fff; padding: 12px 15px; border: 1px solid #ccc; max-width: 800px; border-radius: 4px; margin-bottom: 15px;">';
    echo '<strong style="display: block; margin-bottom: 8px;">Tipos de estado a escanear:</strong>';
    echo '<div style="display: flex; flex-wrap: wrap; gap: 12px; align-items: center;" id="ier-status-filters">';
    echo '<label style="font-weight: bold; margin-right: 10px;"><input type="checkbox" id="ier-check-all" checked> [ Todos ]</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="404" checked> 404</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="404 Interno" checked> 404 Interno</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="Timeout / Error Red" checked> Timeout / Error Red</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="403" checked> 403</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="400" checked> 400</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="500" checked> 500</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="502" checked> 502</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="503" checked> 503</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="520" checked> 520</label>';
    echo '<label><input type="checkbox" class="ier-status-cb" value="Otros / Desconocido" checked> Otros</label>';
    echo '</div>';
    echo '</div>';

    echo '<div style="margin-bottom: 15px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">';
    echo '<button id="ier-start-btn" type="button" class="button button-primary">Iniciar Escaneo Continuo</button>';
    echo '<button id="ier-next-10-btn" type="button" class="button button-secondary">Escanear 10 Entradas</button>';
    echo '<button id="ier-stop-btn" type="button" class="button button-secondary" style="display:none;">Pausar</button>';
    echo '<span style="border-left: 1px solid #ccc; height: 24px; margin: 0 5px;"></span>';
    echo '<button id="ier-export-btn" type="button" class="button button-secondary">Exportar JSON</button>';
    echo '<button id="ier-import-trigger-btn" type="button" class="button button-secondary">Importar JSON</button>';
    echo '<input type="file" id="ier-import-file" accept=".json" style="display: none;">';
    echo '</div>';
    
    echo '<div style="margin-top: 15px; background: #fff; padding: 12px; border: 1px solid #ccc; max-width: 600px; border-radius: 4px;">';
    echo '<strong>Estado:</strong> <span id="ier-status-text">Listo para comenzar</span><br>';
    echo '<strong>Progresión:</strong> Procesadas <span id="ier-processed-count">0</span> de <strong>' . $total_posts . '</strong><br>';
    echo '<strong>Última entrada analizada:</strong> <em id="ier-current-title" style="color: #2271b1;">-</em><br>';
    echo '<strong>Enlaces rotos localizados:</strong> <span id="ier-count-broken" style="color: #d63638; font-weight: bold;">0</span>';
    echo '</div>';

    echo '<div style="margin-top: 20px; display: flex; align-items: center; gap: 10px; background: #f6f7f7; padding: 10px; border: 1px solid #c3c4c7; border-radius: 4px;">';
    echo '<strong>Filtrar vista por tipo:</strong> ';
    echo '<select id="ier-view-filter-select" style="max-width: 250px;"><option value="ALL">Mostrar todos los estados</option></select> ';
    echo '<span id="ier-view-filter-count" style="color: #646970; font-style: italic;"></span>';
    echo '</div>';

    echo '<table class="widefat fixed striped" style="margin-top: 10px;">';
    echo '<thead><tr>';
    echo '<th style="width: 18%;">Post / Página</th>';
    echo '<th style="width: 18%;">Texto / Anchor</th>';
    echo '<th style="width: 24%;">URL Rota</th>';
    echo '<th style="width: 12%;">Estado</th>';
    echo '<th style="width: 23%;">Sustituir por (Wayback/Nuevo)</th>';
    echo '<th style="width: 5%; text-align: center;">Acción</th>';
    echo '</tr></thead>';
    echo '<tbody id="ier-results-body"><tr><td colspan="6">Haz clic en un botón de arriba para comenzar o importa un JSON guardado.</td></tr></tbody>';
    echo '</table>';
    echo '</div>';

    ?>
    <script>
    jQuery(document).ready(function($) {
        const totalPosts = <?php echo $total_posts; ?>;
        const ajaxUrl = '<?php echo esc_url($ajax_url); ?>';
        let lastId = 0;
        let processedCount = 0;
        let scanning = false;
        let totalBroken = 0;
        let targetBatchCount = 0;
        let currentBatchProgress = 0;

        $('#ier-check-all').on('change', function() {
            $('.ier-status-cb').prop('checked', $(this).is(':checked'));
        });

        $('.ier-status-cb').on('change', function() {
            if (!$(this).is(':checked')) {
                $('#ier-check-all').prop('checked', false);
            } else {
                if ($('.ier-status-cb:not(:checked)').length === 0) {
                    $('#ier-check-all').prop('checked', true);
                }
            }
        });

        function getSelectedStatuses() {
            const selected = [];
            $('.ier-status-cb:checked').each(function() {
                selected.push($(this).val());
            });
            return selected;
        }

        function updateViewFilterDropdown() {
            const currentSelected = $('#ier-view-filter-select').val() || 'ALL';
            const statusSet = new Set();

            $('#ier-results-body tr').each(function() {
                const status = $(this).attr('data-status');
                if (status) statusSet.add(status);
            });

            const $select = $('#ier-view-filter-select');
            $select.empty();
            $select.append('<option value="ALL">Mostrar todos los estados</option>');

            Array.from(statusSet).sort().forEach(function(status) {
                $select.append('<option value="' + status + '">' + status + '</option>');
            });

            if (statusSet.has(currentSelected)) {
                $select.val(currentSelected);
            } else {
                $select.val('ALL');
            }

            applyViewFilter();
        }

        function applyViewFilter() {
            const selectedStatus = $('#ier-view-filter-select').val();
            let visibleCount = 0;

            $('#ier-results-body tr').each(function() {
                const $tr = $(this);
                const rowStatus = $tr.attr('data-status');

                if (!rowStatus) return;

                if (selectedStatus === 'ALL' || rowStatus === selectedStatus) {
                    $tr.show();
                    visibleCount++;
                } else {
                    $tr.hide();
                }
            });

            if (selectedStatus === 'ALL') {
                $('#ier-view-filter-count').text('');
            } else {
                $('#ier-view-filter-count').text('(' + visibleCount + ' de ' + totalBroken + ' visibles)');
            }
        }

        $('#ier-view-filter-select').on('change', function() {
            applyViewFilter();
        });

        $(document).on('click', '.ier-replace-btn', function(e) {
            e.preventDefault();
            const $btn = $(this);
            const $tr = $btn.closest('tr');
            
            let postId = parseInt($tr.attr('data-post-id'), 10) || 0;
            if (!postId) {
                const editUrl = $tr.find('.ier-edit-link').attr('href') || '';
                const match = editUrl.match(/post=(\d+)/);
                if (match) {
                    postId = parseInt(match[1], 10);
                }
            }

            const oldUrl = $tr.attr('data-old-url');
            const $input = $tr.find('.ier-new-url-input');
            const newUrl = $input.val().trim();

            if (!postId) {
                alert('Error: ID del Post no válido o ausente.');
                return;
            }

            if (!oldUrl) {
                alert('Error: URL original no encontrada en el elemento.');
                return;
            }

            if (!newUrl) {
                alert('Introduce una nueva URL para reemplazar.');
                return;
            }

            $btn.prop('disabled', true).text('Guardando...');
            $input.prop('disabled', true);

            $.ajax({
                url: ajaxUrl,
                type: 'POST',
                data: {
                    action: 'ier_replace_link',
                    post_id: postId,
                    old_url: oldUrl,
                    new_url: newUrl
                },
                success: function(response) {
                    if (response.success) {
                        $tr.attr('data-old-url', newUrl);
                        $input.prop('disabled', false);
                        $tr.css({'background-color': '#d4edda'});

                        $btn.text('¡Guardado!')
                            .css({'background': '#28a745', 'border-color': '#28a745', 'color': '#fff'})
                            .prop('disabled', false);

                        setTimeout(function() {
                            $btn.text('Guardar')
                                .css({'background': '', 'border-color': '', 'color': ''});
                            $tr.css({'background-color': ''});
                        }, 2000);
                    } else {
                        alert('Error al reemplazar: ' + (response.data || 'No se encontró la URL en la entrada.'));
                        $btn.prop('disabled', false).text('Guardar');
                        $input.prop('disabled', false);
                    }
                },
                error: function() {
                    alert('Error en la solicitud AJAX.');
                    $btn.prop('disabled', false).text('Guardar');
                    $input.prop('disabled', false);
                }
            });
        });

        $(document).on('click', '.ier-unlink-btn', function(e) {
            e.preventDefault();
            const $btn = $(this);
            const $tr = $btn.closest('tr');

            let postId = parseInt($tr.attr('data-post-id'), 10) || 0;
            if (!postId) {
                const editUrl = $tr.find('.ier-edit-link').attr('href') || '';
                const match = editUrl.match(/post=(\d+)/);
                if (match) {
                    postId = parseInt(match[1], 10);
                }
            }

            const urlToUnlink = $tr.attr('data-old-url');

            if (!postId || !urlToUnlink) {
                alert('Error: Datos del post o URL no encontrados.');
                return;
            }

            if (!confirm('¿Quieres eliminar la etiqueta de enlace mantención el texto ancla intacto?')) {
                return;
            }

            $btn.prop('disabled', true).text('Quitando...');

            $.ajax({
                url: ajaxUrl,
                type: 'POST',
                data: {
                    action: 'ier_unlink_link',
                    post_id: postId,
                    url: urlToUnlink
                },
                success: function(response) {
                    if (response.success) {
                        $tr.css({
                            'background-color': '#f8d7da',
                            'transition': 'background-color 0.4s ease'
                        });
                        $btn.text('Enlace quitado').css({'background': '#d63638', 'border-color': '#d63638', 'color': '#fff'});
                        setTimeout(function() {
                            $tr.fadeOut(300, function() {
                                $(this).remove();
                                if (totalBroken > 0) {
                                    totalBroken--;
                                    $('#ier-count-broken').text(totalBroken);
                                }
                                updateViewFilterDropdown();
                            });
                        }, 1000);
                    } else {
                        alert('Error al desvincular: ' + (response.data || 'No se pudo eliminar el enlace.'));
                        $btn.prop('disabled', false).text('Quitar enlace');
                    }
                },
                error: function() {
                    alert('Error en la solicitud AJAX.');
                    $btn.prop('disabled', false).text('Quitar enlace');
                }
            });
        });

        $('#ier-start-btn').on('click', function(e) {
            e.preventDefault();
            if (getSelectedStatuses().length === 0) {
                alert('Selecciona al menos un tipo de estado para escanear.');
                return;
            }
            scanning = true;
            targetBatchCount = totalPosts;
            currentBatchProgress = 0;
            toggleButtons(true);
            if (processedCount === 0) $('#ier-results-body').empty();
            processNext();
        });

        $('#ier-next-10-btn').on('click', function(e) {
            e.preventDefault();
            if (getSelectedStatuses().length === 0) {
                alert('Selecciona al menos un tipo de estado para escanear.');
                return;
            }
            scanning = true;
            targetBatchCount = 10;
            currentBatchProgress = 0;
            toggleButtons(true);
            if (processedCount === 0) $('#ier-results-body').empty();
            processNext();
        });

        $('#ier-stop-btn').on('click', function(e) {
            e.preventDefault();
            scanning = false;
            toggleButtons(false);
            $('#ier-status-text').text('Pausado por el usuario.');
        });

        $(document).on('click', '.ier-dismiss-row', function(e) {
            e.preventDefault();
            $(this).closest('tr').fadeOut(200, function() {
                $(this).remove();
                if (totalBroken > 0) {
                    totalBroken--;
                    $('#ier-count-broken').text(totalBroken);
                }
                updateViewFilterDropdown();
                if ($('#ier-results-body tr').length === 0) {
                    $('#ier-results-body').append('<tr><td colspan="6">No hay enlaces rotos pendientes en el listado.</td></tr>');
                }
            });
        });

        $('#ier-export-btn').on('click', function(e) {
            e.preventDefault();
            const rows = [];

            $('#ier-results-body tr').each(function() {
                const $tr = $(this);
                if ($tr.find('td').length < 6) return;

                const $postLink = $tr.find('.ier-edit-link');
                const postTitle = $postLink.text();
                const postEditUrl = $postLink.attr('href') || '';
                const postViewUrl = $tr.find('.ier-view-link').attr('href') || '';
                
                let postId = parseInt($tr.attr('data-post-id'), 10) || 0;
                if (!postId && postEditUrl) {
                    const match = postEditUrl.match(/post=(\d+)/);
                    if (match) postId = parseInt(match[1], 10);
                }

                const anchorHtml = $tr.find('td:nth-child(2)').html();
                const url = $tr.attr('data-old-url') || $tr.find('td:nth-child(3) a').text();
                const statusHtml = $tr.attr('data-status') || $tr.find('td:nth-child(4) strong').text();

                rows.push({
                    post_id: postId,
                    post_title: postTitle,
                    post_edit_url: postEditUrl,
                    post_view_url: postViewUrl,
                    anchor: anchorHtml,
                    url: url,
                    status: statusHtml
                });
            });

            if (rows.length === 0) {
                alert('No hay enlaces rotos en la lista para exportar.');
                return;
            }

            const exportData = {
                created_at: new Date().toISOString(),
                last_id: lastId,
                processed_count: processedCount,
                total_broken: rows.length,
                broken_links: rows
            };

            const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportData, null, 2));
            const downloadAnchor = document.createElement('a');
            const dateSuffix = new Date().toISOString().slice(0, 10);
            
            downloadAnchor.setAttribute("href", dataStr);
            downloadAnchor.setAttribute("download", "enlaces-rotos-" + dateSuffix + ".json");
            document.body.appendChild(downloadAnchor);
            downloadAnchor.click();
            downloadAnchor.remove();
        });

        $('#ier-import-trigger-btn').on('click', function(e) {
            e.preventDefault();
            $('#ier-import-file').click();
        });

        $('#ier-import-file').on('change', function(e) {
            const file = e.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = function(evt) {
                try {
                    const data = JSON.parse(evt.target.result);
                    if (!data.broken_links || !Array.isArray(data.broken_links)) {
                        alert('El archivo JSON no tiene un formato válido.');
                        return;
                    }

                    $('#ier-results-body').empty();
                    totalBroken = data.broken_links.length;
                    $('#ier-count-broken').text(totalBroken);

                    if (typeof data.last_id !== 'undefined') lastId = data.last_id;
                    if (typeof data.processed_count !== 'undefined') {
                        processedCount = data.processed_count;
                        $('#ier-processed-count').text(processedCount);
                    }

                    data.broken_links.forEach(function(item) {
                        appendRow(item);
                    });

                    updateViewFilterDropdown();
                    $('#ier-status-text').text('Datos importados correctamente desde JSON.');
                    $('#ier-import-file').val('');
                } catch (err) {
                    alert('Error al leer el archivo JSON: ' + err.message);
                }
            };
            reader.readAsText(file);
        });

        function escapeAttr(str) {
            return String(str || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
        }

        function appendRow(item) {
            const cleanStatus = $('<div>').html(item.status).text();
            
            let postId = parseInt(item.post_id, 10) || 0;
            if (!postId && item.post_edit_url) {
                const match = item.post_edit_url.match(/post=(\d+)/);
                if (match) {
                    postId = parseInt(match[1], 10);
                }
            }

            let rawUrl = item.url;
            try {
                rawUrl = decodeURIComponent(item.url);
            } catch (e) {
                rawUrl = item.url;
            }

            const archiveSearchUrl = 'https://web.archive.org/web/*/' + rawUrl;
            const viewUrl = (item.post_view_url && item.post_view_url !== '#') ? item.post_view_url : '<?php echo esc_js(home_url('/?p=')); ?>' + postId;

            $('#ier-results-body').append(
                '<tr data-status="' + escapeAttr(cleanStatus) + '" data-post-id="' + postId + '" data-old-url="' + escapeAttr(item.url) + '">' +
                    '<td>' +
                        '<a href="' + escapeAttr(item.post_edit_url) + '" target="_blank" class="ier-edit-link">' + item.post_title + '</a> ' +
                        '<a href="' + escapeAttr(viewUrl) + '" target="_blank" class="ier-view-link" style="font-size: 11px; text-decoration: none; color: #2271b1;" title="Ver entrada pública">(Ver)</a>' +
                    '</td>' +
                    '<td>' + item.anchor + '</td>' +
                    '<td><a href="' + escapeAttr(item.url) + '" target="_blank" style="color:#d63638;">' + item.url + '</a></td>' +
                    '<td><strong>' + item.status + '</strong></td>' +
                    '<td>' +
                        '<div style="display: flex; gap: 4px;">' +
                            '<input type="url" class="ier-new-url-input" placeholder="https://..." style="width: 100%; font-size: 11px; padding: 2px 4px;">' +
                            '<button type="button" class="button button-small ier-replace-btn" title="Reemplazar URL en la BD">Guardar</button>' +
                        '</div>' +
                        '<div style="margin-top: 4px; display: flex; justify-content: space-between; align-items: center;">' +
                            '<a href="' + escapeAttr(archiveSearchUrl) + '" target="_blank" style="font-size: 10px; color: #2271b1; text-decoration: none;">🔍 Buscar en Archive.org</a>' +
                            '<button type="button" class="button-link ier-unlink-btn" style="font-size: 10px; color: #d63638; text-decoration: underline;" title="Quitar enlace y dejar solo texto">Quitar enlace</button>' +
                        '</div>' +
                    '</td>' +
                    '<td style="text-align: center;"><button type="button" class="button-link ier-dismiss-row" title="Descartar de la lista" style="color: #a7aaad; text-decoration: none; font-size: 18px; font-weight: bold; line-height: 1;">&times;</button></td>' +
                '</tr>'
            );
        }

        function processNext() {
            if (!scanning || currentBatchProgress >= targetBatchCount || processedCount >= totalPosts) {
                scanning = false;
                toggleButtons(false);
                $('#ier-status-text').text(processedCount >= totalPosts ? 'Escaneo completado.' : 'Lote procesado correctamente.');
                return;
            }

            $('#ier-status-text').text('Consultando ID superior a ' + lastId + '...');

            $.ajax({
                url: ajaxUrl,
                type: 'POST',
                data: {
                    action: 'ier_scan_single_post',
                    last_id: lastId,
                    allowed_statuses: getSelectedStatuses()
                },
                success: function(response) {
                    if (response.success) {
                        const data = response.data;

                        if (data.finished) {
                            scanning = false;
                            toggleButtons(false);
                            $('#ier-status-text').text('Fin de la base de datos.');
                            return;
                        }

                        lastId = data.post_id;
                        processedCount++;
                        currentBatchProgress++;

                        $('#ier-processed-count').text(processedCount);
                        $('#ier-current-title').text(data.post_title + ' (ID ' + data.post_id + ')');

                        if (data.broken_links && data.broken_links.length > 0) {
                            if ($('#ier-results-body tr td').length === 1) {
                                $('#ier-results-body').empty();
                            }

                            totalBroken += data.broken_links.length;
                            $('#ier-count-broken').text(totalBroken);

                            data.broken_links.forEach(function(item) {
                                appendRow(item);
                            });

                            updateViewFilterDropdown();
                        }

                        if (scanning) {
                            setTimeout(processNext, 20);
                        }
                    } else {
                        $('#ier-status-text').text('Error del servidor: ' + (response.data || 'Respuesta vacía'));
                        scanning = false;
                        toggleButtons(false);
                    }
                },
                error: function(xhr, status, error) {
                    console.warn('Fallo en ID > ' + lastId + '. Forzando salto al siguiente ID...', status, error);
                    $('#ier-status-text').text('Timeout en ID > ' + lastId + '. Saltando entrada problemática...');
                    
                    lastId++;
                    processedCount++;
                    currentBatchProgress++;
                    
                    if (scanning) {
                        setTimeout(processNext, 1000);
                    }
                }
            });
        }

        function toggleButtons(isScanning) {
            if (isScanning) {
                $('#ier-start-btn, #ier-next-10-btn, #ier-export-btn, #ier-import-trigger-btn').hide();
                $('.ier-status-cb, #ier-check-all').prop('disabled', true);
                $('#ier-stop-btn').show();
            } else {
                $('#ier-stop-btn').hide();
                $('.ier-status-cb, #ier-check-all').prop('disabled', false);
                $('#ier-start-btn, #ier-next-10-btn, #ier-export-btn, #ier-import-trigger-btn').show();
                $('#ier-start-btn').text('Continuar Escaneo');
            }
        }
    });
    </script>
    <?php
}

// 4. Servidor: Analizar entrada individual
function ier_scan_single_post_callback() {
    if (!current_user_can('manage_options')) {
        wp_send_json_error('Sin permisos');
    }

    if (function_exists('wp_ob_end_flush_all')) {
        @wp_ob_end_flush_all();
    }

    @set_time_limit(15);

    global $wpdb;
    $last_id = isset($_POST['last_id']) ? intval($_POST['last_id']) : 0;
    $allowed_statuses = isset($_POST['allowed_statuses']) && is_array($_POST['allowed_statuses']) ? $_POST['allowed_statuses'] : [];

    $post = $wpdb->get_row($wpdb->prepare(
        "SELECT ID, post_title, post_content FROM {$wpdb->posts} WHERE post_type IN ('post', 'page') AND post_status = 'publish' AND ID > %d ORDER BY ID ASC LIMIT 1",
        $last_id
    ));

    if (!$post) {
        wp_send_json_success(['finished' => true]);
    }

    $title = $post->post_title;
    $content = $post->post_content;
    $broken_links = [];

    $post_view_url = get_permalink($post->ID);

    if (!empty($content)) {
        $regex = '/<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is';

        if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
            $site_url = site_url();
            $site_host = parse_url($site_url, PHP_URL_HOST);
            $start_time = microtime(true);

            foreach ($matches as $match) {
                if ((microtime(true) - $start_time) > 3.0) {
                    break;
                }

                $url = trim($match[1]);
                $anchor = wp_strip_all_tags($match[2]);

                if (empty($url) || strpos($url, '#') === 0 || strpos($url, 'mailto:') === 0 || strpos($url, 'tel:') === 0 || strpos($url, 'javascript:') === 0) {
                    continue;
                }

                $url_host = parse_url($url, PHP_URL_HOST);

                // 1. COMPROBACIÓN NATIVA DE ENLACES INTERNOS
                if (empty($url_host) || $url_host === $site_host) {
                    $target_post_id = url_to_postid($url);
                    if (!$target_post_id && !ier_is_internal_url_valid($url)) {
                        $status_key = '404 Interno';
                        if (ier_is_status_allowed($status_key, $allowed_statuses)) {
                            $broken_links[] = [
                                'post_id'       => $post->ID,
                                'post_title'    => $title,
                                'post_edit_url' => get_edit_post_link($post->ID, ''),
                                'post_view_url' => $post_view_url,
                                'anchor'        => !empty($anchor) ? $anchor : '<em>[Sin texto / Imagen]</em>',
                                'url'           => $url,
                                'status'        => $status_key
                            ];
                        }
                    }
                    continue;
                }

                // 2. COMPROBACIÓN RÁPIDA VÍA HEAD
                $args = [
                    'timeout'     => 1,
                    'redirection' => 1,
                    'sslverify'   => false,
                    'user-agent'  => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
                ];

                $response = wp_remote_head($url, $args);
                $status_code = is_wp_error($response) ? 0 : wp_remote_retrieve_response_code($response);

                if (is_wp_error($response) || $status_code === 405 || $status_code === 403) {
                    $response = wp_remote_get($url, $args);
                    $status_code = is_wp_error($response) ? 'Timeout / Error Red' : wp_remote_retrieve_response_code($response);
                }

                if (intval($status_code) === 498) {
                    continue;
                }

                if (is_wp_error($response) || (is_numeric($status_code) && $status_code >= 400) || $status_code === 'Timeout / Error Red') {
                    $status_str = (string) $status_code;
                    
                    if (ier_is_status_allowed($status_str, $allowed_statuses)) {
                        $broken_links[] = [
                            'post_id'       => $post->ID,
                            'post_title'    => $title,
                            'post_edit_url' => get_edit_post_link($post->ID, ''),
                            'post_view_url' => $post_view_url,
                            'anchor'        => !empty($anchor) ? $anchor : '<em>[Sin texto / Imagen]</em>',
                            'url'           => $url,
                            'status'        => $status_str
                        ];
                    }
                }
            }
        }
    }

    wp_send_json_success([
        'post_id'      => $post->ID,
        'post_title'   => $title,
        'broken_links' => $broken_links,
        'finished'     => false
    ]);
}

// 5. Servidor: Reemplazar URL directamente en el contenido del post
function ier_replace_link_callback() {
    if (!current_user_can('manage_options')) {
        wp_send_json_error('Sin permisos');
    }

    global $wpdb;
    $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
    
    $old_url_raw = isset($_POST['old_url']) ? wp_unslash(trim($_POST['old_url'])) : '';
    $new_url_raw = isset($_POST['new_url']) ? wp_unslash(trim($_POST['new_url'])) : '';

    if (!$post_id || empty($old_url_raw) || empty($new_url_raw)) {
        wp_send_json_error('Parámetros incompletos.');
    }

    $content = $wpdb->get_var($wpdb->prepare("SELECT post_content FROM {$wpdb->posts} WHERE ID = %d", $post_id));

    if (!$content) {
        wp_send_json_error('No se encontró el contenido de la entrada.');
    }

    $old_url_decoded = html_entity_decode($old_url_raw, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $old_url_entities = htmlentities($old_url_decoded, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $old_url_amp = str_replace('&', '&amp;', $old_url_decoded);
    $old_url_sanitized = esc_url_raw($old_url_raw);

    $target_old_url = false;

    if (strpos($content, $old_url_raw) !== false) {
        $target_old_url = $old_url_raw;
    } elseif (strpos($content, $old_url_decoded) !== false) {
        $target_old_url = $old_url_decoded;
    } elseif (strpos($content, $old_url_amp) !== false) {
        $target_old_url = $old_url_amp;
    } elseif (strpos($content, $old_url_entities) !== false) {
        $target_old_url = $old_url_entities;
    } elseif (strpos($content, $old_url_sanitized) !== false) {
        $target_old_url = $old_url_sanitized;
    }

    if ($target_old_url !== false) {
        $new_url = esc_url_raw($new_url_raw);
        $updated_content = str_replace($target_old_url, $new_url, $content);
        
        $result = $wpdb->update(
            $wpdb->posts,
            ['post_content' => $updated_content],
            ['ID' => $post_id],
            ['%s'],
            ['%d']
        );

        if ($result !== false) {
            clean_post_cache($post_id);
            wp_send_json_success();
        } else {
            wp_send_json_error('No se pudo actualizar la base de datos.');
        }
    } else {
        wp_send_json_error('La URL original no coincide exactamente con el texto en BD.');
    }
}

// 6. Servidor: Eliminar enlace conservando el texto ancla (Unlink)
function ier_unlink_link_callback() {
    if (!current_user_can('manage_options')) {
        wp_send_json_error('Sin permisos');
    }

    global $wpdb;
    $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
    $url_raw = isset($_POST['url']) ? wp_unslash(trim($_POST['url'])) : '';

    if (!$post_id || empty($url_raw)) {
        wp_send_json_error('Parámetros incompletos.');
    }

    $content = $wpdb->get_var($wpdb->prepare("SELECT post_content FROM {$wpdb->posts} WHERE ID = %d", $post_id));

    if (!$content) {
        wp_send_json_error('No se encontró la entrada.');
    }

    $url_decoded = html_entity_decode($url_raw, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $url_sanitized = esc_url_raw($url_raw);
    
    // Lista de variaciones posibles de la URL para que la expresión regular la capture bien
    $urls_to_check = array_unique([$url_raw, $url_decoded, $url_sanitized]);
    $replaced = false;
    $updated_content = $content;

    foreach ($urls_to_check as $target_url) {
        $quoted_url = preg_quote($target_url, '/');
        $pattern = '/<a\s+[^>]*href=["\']' . $quoted_url . '["\'][^>]*>(.*?)<\/a>/is';

        if (preg_match($pattern, $updated_content)) {
            $updated_content = preg_replace($pattern, '$1', $updated_content);
            $replaced = true;
            break;
        }
    }

    if ($replaced) {
        $result = $wpdb->update(
            $wpdb->posts,
            ['post_content' => $updated_content],
            ['ID' => $post_id],
            ['%s'],
            ['%d']
        );

        if ($result !== false) {
            clean_post_cache($post_id);
            wp_send_json_success();
        } else {
            wp_send_json_error('No se pudo actualizar la base de datos.');
        }
    } else {
        wp_send_json_error('No se pudo encontrar el enlace exacto en el contenido del post.');
    }
}

// Funciones auxiliares
function ier_clean_raw_url($url) {
    $url = wp_unslash(trim($url));
    if (empty($url)) return '';
    return esc_url_raw($url);
}

function ier_is_status_allowed($status, $allowed_statuses) {
    if (empty($allowed_statuses)) return true;
    
    $status_str = (string) $status;
    
    if (in_array($status_str, $allowed_statuses, true)) {
        return true;
    }
    
    if (in_array('Otros / Desconocido', $allowed_statuses, true)) {
        $known_statuses = ['404', '404 Interno', 'Timeout / Error Red', '403', '400', '500', '502', '503', '520'];
        if (!in_array($status_str, $known_statuses, true)) {
            return true;
        }
    }
    
    return false;
}

function ier_is_internal_url_valid($url) {
    // Si es solo una ruta con query (ej: ?p=123) o raíz
    $path = parse_url($url, PHP_URL_PATH);
    if (empty($path) || $path === '/') return true;

    // 1. Comprobar si existe físicamente en el servidor (ej: /archivo/documento.pdf, /archivo/letras.doc, etc.)
    $site_host = parse_url(site_url(), PHP_URL_HOST);
    $url_host  = parse_url($url, PHP_URL_HOST);
    
    if (empty($url_host) || $url_host === $site_host) {
        $local_path = ABSPATH . ltrim($path, '/');
        if (file_exists($local_path) || is_dir($local_path)) {
            return true;
        }
    }

    // 2. Comprobar si es un Post, Página, Adjunto o Custom Post Type por Slug/Path (soporta .htm/.html si el slug lo incluye)
    $clean_path = ltrim($path, '/');
    $page_or_post = get_page_by_path($clean_path, OBJECT, ['post', 'page', 'attachment']);
    if (!$page_or_post && (str_ends_with($clean_path, '.htm') || str_ends_with($clean_path, '.html'))) {
        $path_without_ext = preg_replace('/\.html?$/i', '', $clean_path);
        $page_or_post = get_page_by_path($path_without_ext, OBJECT, ['post', 'page', 'attachment']);
    }
    if ($page_or_post) return true;

    // 3. Comprobar si es una Taxonomía (etiquetas /tag/, categorías /category/, etc.)
    $term_path = trim($path, '/');
    $segments  = explode('/', $term_path);
    if (count($segments) >= 2) {
        $taxonomy_slug = $segments[0];
        $term_slug     = end($segments);
        
        // Mapeo común de slugs de taxonomía
        $taxonomies = [
            'tag'       => 'post_tag',
            'category'  => 'category',
            'etiqueta'  => 'post_tag',
            'categoria' => 'category'
        ];

        $target_tax = isset($taxonomies[$taxonomy_slug]) ? $taxonomies[$taxonomy_slug] : $taxonomy_slug;

        if (taxonomy_exists($target_tax) && term_exists($term_slug, $target_tax)) {
            return true;
        }
    }

    // 4. Intento de resolución nativa vía Rewrite Rules de WordPress
    $query_vars = [];
    if (class_exists('WP')) {
        $wp = new WP();
        $query_vars = $wp->parse_request($url);
    }
    
    if (!empty($query_vars)) {
        return true;
    }

    // 5. Comprobar directorio de subidas (/wp-content/uploads/)
    $upload_dir = wp_upload_dir();
    $base_url   = parse_url($upload_dir['baseurl'], PHP_URL_PATH);
    if ($base_url && strpos($path, $base_url) === 0) {
        $relative_file = str_replace($base_url, '', $path);
        $file_path     = $upload_dir['basedir'] . $relative_file;
        if (file_exists($file_path)) return true;
    }

    return false;
}

Este blog se aloja en LucusHost

LucusHost,el mejor hosting