Trasiga länkar leder till förlust av auktoritet (link juice) och slukar krypningsbudgeten (crawl budget), eftersom sökmotorernas botar slösar tid på att indexera HTTP 404-svar istället för värdefullt innehåll. De försämrar också användarupplevelsen genom att avbryta navigationsflödet och öka avvisningsfrekvensen. Genom att åtgärda dem med 301-omdirigeringar eller uppdatering av URL:er optimeras webbplatsens genomsökning, flödet av länkvärde till målsidorna bevaras och webbplatsens arkitektur stärks för att undvika indirekta straff i den organiska rankningen.
När jag gjorde min rutinmässiga genomgång av brutna länkar med det vanliga tillägget, Broken Link Checker, upptäckte jag att den lokala genomsökningen inte längre fungerade, troligen på grund av något skydd på servern. Så jag tog fram en väldigt enkel funktion som jag hade liggande och förbättrade den lite med hjälp av Gemini för att göra den mer användbar och kunna köra genomsökningen och korrigeringen av brutna länkar.
Även om brutna länkar dyker upp som svampar ur jorden har jag under den här resan märkt en betydande minskning av länkar från Twitter (X), som förlorar konton i rasande takt. Det dyker redan upp hundratals konton och/eller tweets som inte längre finns, men jag sparar den här frågan till ett kommande inlägg om nätets död, så som vi kände det.

För att använda funktionen behöver du bara lägga till kodsnuttet i filen functions.php i din mall eller ditt underordnade tema, eller i ditt kodsnutt-plugin. Under Verktyg/Länkinspektör hittar du verktyget och dess alternativ, som beskrivs nedan.
1 – Statustyper. Här kan du välja mellan mer än 10 olika statustyper för en selektiv skanning.
2 – Knappen ”Starta kontinuerlig skanning” startar en skanning utan avbrott. Även om det inte borde vara fallet kan det ha en viss inverkan på webbplatsens laddningshastighet. Jag har låtit den vara igång i flera timmar utan problem.
3 – Skanning i omgångar. Varje gång du trycker på den här knappen skannas 10 poster.
4 – Exportera/importera JSON. Eftersom tillägget inte sparar något i databasen kommer listan att gå förlorad om du stänger webbläsaren. För att återställa allt som du inte minns att du har gjort måste du alltid spara din skanningslista med funktionen ”Exportera”. För att återställa den räcker det att importera den.
5 – Information om skanningsprocessen.
6 – Filtrering efter typ. När du har avslutat skanningen kan du filtrera listvyn efter status.
7 – Inlägg/sida. Här finns en länk för att redigera inlägget och en länk för att visa det.
8 – Länkens ankare, så att du snabbare kan hitta den.
9 – URL:en till den trasiga länken.
10 – Länkens HTTP-statuskod.
11 – Åtgärd. Stängningsikonen tar bara bort den raden från listvyn. Den redigerar, raderar eller lägger inte till något.
12 – Ersättnings-URL. Fält där ersättningslänken anges.
13 – Sök på Archive.org. När du klickar här öppnas Wayback Machine med en lista över tillgängliga datum för kopiorna av din webbadress (om sådana finns bevarade), så att du kan välja det datum som intresserar dig mest.
14 – Ta bort länk. Det här alternativet tar bort hyperlänken men behåller länktexten i inlägget eller på sidan, ifall du inte har någon alternativ URL eller inte vill lägga till en.
Låt mig först säga att det här är en engångsfunktion, så det rekommenderas att du tar bort den när du är klar med rengöringen. Jag tror inte att jag kommer att ändra den ytterligare, men om jag skulle göra det skulle jag uppdatera koden här.
Även om koden till slut blev lite för lång för ett ”snippet” – som ju egentligen ska vara kort eller ett utdrag – så fungerar den ganska bra. Visst kan den fortfarande finjusteras för att undvika vissa falska 404-fel och förbättras med möjligheten att välja flera åtgärder samtidigt, men om du inte har så många länkar och tid att granska dem manuellt är den fullt fungerande.
För att ersätta ett stort antal länkar kan du använda klassiska specialiserade plugins som Better Search Replace och Search Regex.
Uppdatering den 23 augusti 2026.
Förbättringar:
1 – En ruta för flerval har lagts till till vänster samt en knapp medtexten ”Avmarkera valda” för att ta bort rader från listan i omgångar.
2 – Svarstiden för länkarna till Archive.org och Bluesky justeras till 7 sekunder för att undvika att resultaten fylls med 403-fel eller ”Timeout”-meddelanden från dessa domäner.
3 – Processen för att ladda upp JSON-filen i funktionen ”Importera JSON” har optimerats
Uppdatering den 24 augusti 2026
Nytt system för uteslutning av domäner (IP-skydd): En robust uteslutningsmatris har integrerats för att ignorera stora webbplatser och sociala nätverk.
Tyst hopp: Från och med nu kommer skannern, om den upptäcker en länk som pekar mot Google, YouTube, Twitter/X, Facebook, Instagram, LinkedIn, Archive.org, Amazon, Pinterest eller TikTok, automatiskt att hoppa över den utan att skicka någon HTTP-förfrågan till dessa webbplatser. Du måste alltså kontrollera dessa länkar med ett annat verktyg eller manuellt.
Inga falska positiva resultat och inga blockeringar: Denna åtgärd förhindrar att din servers IP-adress tillfälligt hamnar på svartlistor på grund av för många förfrågningar (rate limiting, såsom 429-blockeringar i Googles API:er). Dessutom rensar den bort resultaten från ”falska brutna länkar” (403-fel) som orsakas av dessa plattformars aggressiva brandväggar mot botar, vilket gör den allmänna genomsökningen mycket snabbare och effektivare.
// 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;
}





