I link non funzionanti causano perdite di autorità (link juice) e consumano il budget di scansione (crawl budget), poiché i bot dei motori di ricerca perdono tempo a indicizzare risposte HTTP 404 invece di contenuti di valore. Inoltre, compromettono l’esperienza utente interrompendo il flusso di navigazione e aumentando la frequenza di rimbalzo. Correggendoli con reindirizzamenti 301 o aggiornando gli URL, si ottimizza la scansione del sito, si preserva il flusso di autorità dei link verso le pagine di destinazione e si consolida l’architettura del sito per evitare penalizzazioni indirette nel posizionamento organico.
Durante il controllo di routine dei link non funzionanti con il solito plugin, Broken Link Checker, ho scoperto che la scansione locale non funzionava. Così ho preso una funzione molto semplice che avevo a disposizione e l'ho migliorata un po' con l'aiuto di Gemini per renderla più fruibile ed eseguire la scansione e la correzione dei link non funzionanti.
Sebbene i link non funzionanti spuntino come funghi, in questo mio percorso ho notato una forte emorragia di link da Twitter (X), che sta perdendo account a raffica. Sono già centinaia gli account e/o i tweet che hanno cessato di esistere, ma rimanderò questo argomento a un prossimo articolo sulla morte del web così come lo conoscevamo.

Per utilizzare questa funzione è sufficiente aggiungere lo snippet al file functions.php del tuo template o tema figlio oppure al tuo plugin di snippet; poi, in Strumenti/Ispeziona link troverai lo strumento e le relative opzioni, descritte in dettaglio di seguito.
1 - Tipi di stato. Qui puoi scegliere tra oltre 10 tipi di stato per una scansione selettiva.
2 - Il pulsante "Avvia scansione continua " avvia una scansione senza interruzioni. Anche se non dovrebbe, potrebbe influire in qualche misura sulla velocità di caricamento del sito. L'ho tenuto in funzione per ore senza alcun problema.
3 - Scansione in batch. Ogni volta che premi questo pulsante, verranno scansionate 10 voci.
4 - Esporta/Importa JSON. Poiché il plugin non salva nulla nel database, se chiudi il browser perderai l'elenco e, per recuperare tutto ciò che non ricordi di aver fatto, dovrai sempre salvare il tuo elenco di scansione con "Esporta". Per recuperarlo basterà importarlo.
5 - Informazioni sul processo di scansione.
6 - Filtro per tipologia. Una volta completata la scansione, puoi filtrare la visualizzazione dell'elenco in base allo stato.
7 - Post/Pagina. Qui troverai un link per modificare il post e un altro link (Visualizza) per visitarlo.
8 - L'anchor del link, così puoi trovarlo più velocemente.
9 - L'URL del link non funzionante.
10 - Codice di stato HTTP del link.
11 - Azione. L'icona a forma di croce rimuove solo quella riga dalla visualizzazione dell'elenco. Non modifica, non cancella né aggiunge nulla.
12 - URL di sostituzione. Campo in cui inserire il link di sostituzione.
13 - Cerca su Archive.org. Cliccando qui si aprirà Wayback Machine con l'elenco delle date disponibili delle copie del tuo URL (se conservate), in modo che tu possa scegliere quella che ti interessa di più.
14 - Rimuovi collegamento. Questa opzione elimina il collegamento ipertestuale, mantenendo il testo del collegamento nel post o nella pagina, nel caso in cui non si disponga di un URL alternativo o non si desideri aggiungerlo.
Premetto che si tratta di una funzione monouso; si consiglia di rimuoverla una volta terminata la pulizia. Non credo che la modificherò ulteriormente, ma se lo facessi aggiornerò il codice proprio qui.
Anche se alla fine il codice è risultato troppo lungo per essere considerato uno snippet — che in teoria dovrebbe essere breve o un frammento — funziona piuttosto bene. Certo, si può ancora perfezionare per evitare alcuni falsi positivi di errore 404 e migliorarlo con la selezione multipla delle azioni, ma se non hai molti link e non hai tempo per controllarli manualmente, è perfettamente funzionante.
Per le sostituzioni in massa dei link, puoi avvalerti dei classici plugin dedicati come Better Search Replace e Search Regex.
Aggiornamento del 23 agosto 2026.
Miglioramenti:
1 - Viene aggiunta una casella di selezione multipla a sinistra e un pulsante“Elimina selezionati” per rimuovere più righe contemporaneamente dall’elenco.
2 - Il tempo di risposta per i link di Archive.org e Bluesky viene impostato a 7 secondi per evitare che compaiano risultati 403 o "Timeout" con questi domini.
3 - È stato ottimizzato il processo di caricamento del file JSON nella sezione "Importa JSON"
Aggiornamento del 24 agosto 2026
Nuovo sistema di esclusione dei domini (Protezione IP): è stato integrato un solido array di esclusione per ignorare i siti web di grandi dimensioni e i social network.
Salto silenzioso: d'ora in poi, se lo scanner rileva un link che rimanda a Google, YouTube, Twitter/X, Facebook, Instagram, LinkedIn, Archive.org, Amazon, Pinterest o TikTok, lo ignorerà automaticamente senza inviare alcuna richiesta HTTP a tali siti. Dovrai quindi controllare questi link con un altro strumento o manualmente.
Zero falsi positivi e nessun blocco: questa misura impedisce che l’IP del tuo server finisca temporaneamente nelle liste nere a causa di un eccesso di richieste (rate limiting, come i blocchi 429 nelle API di Google). Inoltre, elimina i risultati relativi ai “falsi link non funzionanti” (errori 403) causati dagli aggressivi firewall anti-bot di queste piattaforme, rendendo la scansione generale molto più veloce ed efficiente.
// Actualizado 24 de agosto 2026 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 '<style>.ier-hidden-row { display: none !important; }</style>';
echo '<div class="wrap">';
echo '<h1>Inspector de Enlaces Rotos</h1>';
echo '<p>Escaneo ultra-ligero con reemplazo directo, desvinculación, filtro por estados, exportación/importación JSON y protección anti-bloqueos (excluye RRSS y Google).</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 '<span style="border-left: 1px solid #ccc; height: 20px; margin: 0 5px;"></span>';
echo '<button id="ier-bulk-dismiss-btn" type="button" class="button button-secondary">Descartar seleccionados</button>';
echo '</div>';
echo '<table class="widefat fixed striped" style="margin-top: 10px;">';
echo '<thead><tr>';
echo '<th style="width: 3%; text-align: center;"><input type="checkbox" id="ier-bulk-select-all" title="Seleccionar todos los visibles"></th>';
echo '<th style="width: 17%;">Post / Página</th>';
echo '<th style="width: 16%;">Texto / Anchor</th>';
echo '<th style="width: 23%;">URL Rota</th>';
echo '<th style="width: 12%;">Estado</th>';
echo '<th style="width: 24%;">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 class="ier-empty-row"><td colspan="7">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;
}
$('#ier-bulk-select-all').on('change', function() {
$('.ier-bulk-cb:visible').prop('checked', $(this).is(':checked'));
});
$('#ier-bulk-dismiss-btn').on('click', function(e) {
e.preventDefault();
const $checked = $('.ier-bulk-cb:checked:visible');
if ($checked.length === 0) {
alert('No hay enlaces seleccionados en la vista actual.');
return;
}
if (!confirm('¿Seguro que quieres descartar ' + $checked.length + ' enlaces de la lista?')) {
return;
}
$checked.each(function() {
$(this).closest('tr').remove();
if (totalBroken > 0) totalBroken--;
});
$('#ier-count-broken').text(totalBroken);
$('#ier-bulk-select-all').prop('checked', false);
updateViewFilterDropdown();
if ($('#ier-results-body tr:not(.ier-empty-row)').length === 0) {
$('#ier-results-body').html('<tr class="ier-empty-row"><td colspan="7">No hay enlaces rotos pendientes en el listado.</td></tr>');
}
});
function updateViewFilterDropdown() {
const currentSelected = $('#ier-view-filter-select').val() || 'ALL';
const statusSet = new Set();
$('#ier-results-body tr:not(.ier-empty-row)').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;
const $rows = $('#ier-results-body tr:not(.ier-empty-row)');
if (selectedStatus === 'ALL') {
$rows.removeClass('ier-hidden-row');
visibleCount = $rows.length;
$('#ier-view-filter-count').text('');
} else {
$rows.each(function() {
const $tr = $(this);
if ($tr.attr('data-status') === selectedStatus) {
$tr.removeClass('ier-hidden-row');
visibleCount++;
} else {
$tr.addClass('ier-hidden-row');
}
});
$('#ier-view-filter-count').text('(' + visibleCount + ' de ' + totalBroken + ' visibles)');
}
$('#ier-bulk-select-all').prop('checked', false);
}
$('#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();
if ($('#ier-results-body tr:not(.ier-empty-row)').length === 0) {
$('#ier-results-body').html('<tr class="ier-empty-row"><td colspan="7">No hay enlaces rotos pendientes en el listado.</td></tr>');
}
});
}, 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();
const $tr = $(this).closest('tr');
$tr.fadeOut(150, function() {
$(this).remove();
if (totalBroken > 0) {
totalBroken--;
$('#ier-count-broken').text(totalBroken);
}
updateViewFilterDropdown();
if ($('#ier-results-body tr:not(.ier-empty-row)').length === 0) {
$('#ier-results-body').html('<tr class="ier-empty-row"><td colspan="7">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:not(.ier-empty-row)').each(function() {
const $tr = $(this);
if ($tr.find('td').length < 7) 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(3)').html();
const url = $tr.attr('data-old-url') || $tr.find('td:nth-child(4) a').text();
const statusHtml = $tr.attr('data-status') || $tr.find('td:nth-child(5) 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);
}
let allRowsHtml = '';
data.broken_links.forEach(function(item) {
allRowsHtml += buildRowHtml(item);
});
$('#ier-results-body').html(allRowsHtml);
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, '"').replace(/'/g, ''');
}
function buildRowHtml(item) {
const cleanStatus = typeof item.status === 'string' ? item.status.replace(/<[^>]*>?/gm, '') : item.status;
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;
return '<tr data-status="' + escapeAttr(cleanStatus) + '" data-post-id="' + postId + '" data-old-url="' + escapeAttr(item.url) + '">' +
'<td style="text-align: center;"><input type="checkbox" class="ier-bulk-cb"></td>' +
'<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;">×</button></td>' +
'</tr>';
}
function appendRow(item) {
$('.ier-empty-row').remove();
$('#ier-results-body').append(buildRowHtml(item));
}
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) {
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(20);
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) > 8.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. EXCLUSIÓN DE DOMINIOS GIGANTES (Protección de IP y reducción de falsos positivos)
$excluded_domains = [
'google.com', 'google.es', 'googleapis.com',
'youtube.com', 'youtu.be',
'twitter.com', 'x.com',
'facebook.com', 'fb.com', 'instagram.com', 'linkedin.com',
'archive.org', 'web.archive.org',
'pinterest.com', 'tiktok.com',
'amazon.com', 'amazon.es', 'amzn.to'
];
$is_excluded = false;
foreach ($excluded_domains as $ex_domain) {
if ($url_host === $ex_domain || substr($url_host, -strlen('.' . $ex_domain)) === '.' . $ex_domain) {
$is_excluded = true;
break;
}
}
if ($is_excluded) {
continue; // Saltamos este enlace silenciosamente sin hacer petición HTTP
}
// 3. AJUSTES DINÁMICOS PARA BLUESKY (Archive ya está excluido arriba)
$timeout_limit = 2;
if (strpos($url_host, 'bsky.app') !== false) {
$timeout_limit = 7;
}
// 4. COMPROBACIÓN RÁPIDA VÍA HEAD
$args = [
'timeout' => $timeout_limit,
'redirection' => 2,
'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',
'headers' => [
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language' => 'es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3',
]
];
$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;
}
// Excepción específica para falsos positivos de Bluesky (bloqueo antibots 403)
if (strpos($url_host, 'bsky.app') !== false && in_array($status_code, [403, 'Timeout / Error Red'], true)) {
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('&', '&', $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);
$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) {
$path = parse_url($url, PHP_URL_PATH);
if (empty($path) || $path === '/') return true;
$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;
}
}
$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;
$term_path = trim($path, '/');
$segments = explode('/', $term_path);
if (count($segments) >= 2) {
$taxonomy_slug = $segments[0];
$term_slug = end($segments);
$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;
}
}
$query_vars = [];
if (class_exists('WP')) {
$wp = new WP();
$query_vars = $wp->parse_request($url);
}
if (!empty($query_vars)) {
return true;
}
$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;
}





