Contents
Introducción
Este tutorial describe, con todo lujo de detalles, cómo auditar enlaces rotos en un sitio WordPress y presentar un informe en el área de administración usando PHP. La solución que se expone está pensada para ser robusta y escalable: escanea entradas y otros tipos de contenido, comprueba el estado HTTP de los enlaces, almacena resultados y ofrece una vista administrativa para revisarlos, marcarlos como ignorados o re-comprobarlos.
Visión general de la arquitectura
Componentes principales:
- Escaneo de contenido para extraer URLs (posts, páginas, CPT).
- Comprobación de cada URL mediante peticiones HTTP (HEAD/GET).
- Almacenamiento de resultados en una tabla propia en la base de datos para consultar y filtrar.
- Tareas programadas para re-auditar periódicamente (WP-Cron o WP-CLI).
- Interfaz de administración para ver y gestionar enlaces rotos.
Consideraciones iniciales
- Rendimiento: lanzar miles de solicitudes HTTP en una sola carga puede sobrecargar el servidor y provocar timeouts. Hay que limitar concurrencia y usar cron o procesos por lotes.
- Respeto a terceros: establecer tiempos de espera adecuados, limitar la frecuencia de comprobación a dominios externos y respetar redirecciones.
- Falsos positivos: algunos servidores bloquean HEAD o requieren GET implementar fallback a GET.
- Almacenamiento: usar una tabla propia permite consultas rápidas y persistencia histórica.
Esquema de base de datos
Crear una tabla propia para guardar los resultados de auditoría. Ejemplo de SQL para el activation hook del plugin:
prefix . link_audit charset_collate = wpdb->get_charset_collate() sql = CREATE TABLE {table_name} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT(20) UNSIGNED DEFAULT NULL, url TEXT NOT NULL, anchor_text VARCHAR(255) DEFAULT NULL, is_internal TINYINT(1) DEFAULT 0, http_code SMALLINT DEFAULT NULL, status_text VARCHAR(255) DEFAULT NULL, final_url TEXT DEFAULT NULL, last_checked DATETIME DEFAULT NULL, error_message TEXT DEFAULT NULL, ignored TINYINT(1) DEFAULT 0, PRIMARY KEY (id), KEY post_id (post_id), KEY http_code (http_code), KEY last_checked (last_checked) ) {charset_collate} require_once(ABSPATH . wp-admin/includes/upgrade.php) dbDelta(sql) } register_activation_hook(__FILE__, link_audit_create_table) ?>
Extracción de URLs desde contenido
La extracción usando DOMDocument es más fiable que regex para HTML bien formado. Hay que manejar errores de libxml y asegurar el correcto encoding.
. content dom->loadHTML(html) libxml_clear_errors() anchors = dom->getElementsByTagName(a) links = array() foreach (anchors as a) { href = a->getAttribute(href) if (!href) continue text = trim(a->textContent) links[] = array( url => href, anchor => mb_substr(text, 0, 255) ) } return links } ?>
Determinación de si un enlace es interno
Un helper simple para comprobar si la URL pertenece a tu dominio.
interna return (strtolower(host) === strtolower(site_host)) } ?>
Comprobación del estado HTTP
Usaremos wp_remote_head y, si hace falta, wp_remote_get como fallback. Configuramos timeouts y manejamos redirecciones.
10, redirection => 10, user-agent => WordPress Link Auditor/1.0, sslverify => false, // opcional: en producción conviene true o configurable ) // Intento HEAD response = wp_remote_head(url, args) // Algunos servidores no soportan HEAD correctamente -> fallback a GET if (is_wp_error(response) wp_remote_retrieve_response_code(response) == 0) { response = wp_remote_get(url, args) } if (is_wp_error(response)) { return array( http_code => null, status_text => null, final_url => null, error => response->get_error_message() ) } code = wp_remote_retrieve_response_code(response) final_url = wp_remote_retrieve_header(response, x-final-url) if (!final_url) { // Puede no existir x-final-url usar URL del propio response si es posible final_url = isset(response[http_response]) isset(response[http_response]->response) isset(response[http_response]->response[url]) ? response[http_response]->response[url] : url } return array( http_code => (int) code, status_text => get_status_header_desc(code), final_url => final_url, error => null ) } function get_status_header_desc(code) { map = array( 200 => OK, 301 => Moved Permanently, 302 => Found, 403 => Forbidden, 404 => Not Found, 500 => Internal Server Error, // añadir más si se desea ) return isset(map[code]) ? map[code] : } ?>
Almacenamiento de resultados
Función que inserta/actualiza registros en la tabla creada:
prefix . link_audit // Evitar duplicados: actualizar si existe existing = wpdb->get_row(wpdb->prepare(SELECT id FROM {table} WHERE post_id = %d AND url = %s, post_id, url)) data = array( post_id => post_id, url => url, anchor_text => anchor, is_internal => is_internal ? 1 : 0, http_code => check_result[http_code], status_text => check_result[status_text], final_url => check_result[final_url], last_checked => current_time(mysql), error_message => isset(check_result[error]) ? check_result[error] : null, ) if (existing) { wpdb->update(table, data, array(id => existing->id), null, array(%d)) } else { wpdb->insert(table, data) } } ?>
Escaneo completo (por lotes)
Escanear todo el contenido del sitio es costoso: hacerlo por lotes o programado. Ejemplo de función que procesa posts por lotes:
post_types, posts_per_page => limit, offset => offset, post_status => publish, fields => ids, ) posts = get_posts(args) if (empty(posts)) return 0 foreach (posts as post_id) { post = get_post(post_id) links = link_audit_extract_links_from_content(post->post_content) foreach (links as link) { url = link[url] // Normalizar y omitir anclas internas vacías if (strpos(url, #) === 0) continue is_internal = link_audit_is_internal(url) check = link_audit_check_url(url) link_audit_save_result(post_id, url, link[anchor], is_internal, check) // Opcional: sleep breve para no sobrecargar usleep(100000) // 100ms } } return count(posts) } ?>
Tareas programadas
Registrar un evento de WP-Cron para ejecutar el escaneo en background (por lotes) diariamente o semanalmente:
0) { update_option(link_audit_last_offset, offset processed) } else { // reiniciar ciclo update_option(link_audit_last_offset, 0) } } register_activation_hook(__FILE__, link_audit_activate) register_deactivation_hook(__FILE__, link_audit_deactivate) ?>
Interfaz de administración
Añadir una página bajo el menú Herramientas (Tools) que muestre enlaces rotos y permita acciones:
prefix . link_audit // Filtrar por estado 404 por defecto rows = wpdb->get_results(SELECT FROM {table} WHERE http_code >= 400 AND ignored = 0 ORDER BY last_checked DESC LIMIT 200) echoEnlaces rotos detectados
echo
ID | Post | URL | Código | Último chequeo | Acciones |
---|---|---|---|---|---|
. intval(row->id) . | echo. esc_html(post_title) . (edit_link ? [Editar] : ) . | echo. esc_html(row->url) . | echo. intval(row->http_code) . . esc_html(row->status_text) . | echo. esc_html(row->last_checked) . | echoecho id)) . >Recomprobar echo id)) . >Ignorar echo | echo
Enlace marcado como ignorado.
} if (isset(_GET[recheck])) { id = intval(_GET[recheck]) record = wpdb->get_row(wpdb->prepare(SELECT FROM {table} WHERE id = %d, id)) if (record) { check = link_audit_check_url(record->url) link_audit_save_result(record->post_id, record->url, record->anchor_text, record->is_internal, check) echoRechequeo realizado.
} } } ?>Buenas prácticas en la interfaz de administración
- Usar nonces y verificar current_user_can(manage_options) antes de ejecutar acciones que cambian datos.
- Paginación y filtros por tipo (interno/externo), código HTTP, fecha de último chequeo.
- Exportar CSV para análisis externo.
- Usar WP_List_Table para tablas con paginación y ordenación si se desea una experiencia más profesional.
Escaneo asíncrono y rendimiento
Para sitios grandes conviene:
- Usar Action Scheduler o Background Processing (por ejemplo, wp_async_task o Action Scheduler) para ejecutar comprobaciones fuera del request principal.
- Lanzar comprobaciones por lotes pequeños desde WP-Cron o desde un WP-CLI que se ejecute por cron del sistema (más fiable que WP-Cron).
- Implementar limitación por dominio y respetar cabeceras robots y rate limiting.
Comando WP-CLI para auditoría on-demand
Registrar un comando WP-CLI para lanzar un escaneo completo bajo demanda (útil para grandes sitios):
Ideas de mejoras y funcionalidades adicionales
- Historial de cambios: almacenar cada chequeo para ver evolución.
- Notificaciones por correo a administradores cuando se detecten nuevos enlaces rotos críticos.
- Integración con editores: un enlace rápido Buscar y reemplazar en el editor de post para corregir la URL.
- Opción de ignorar por patrón de URL (ej., dominios de terceros que no quieres auditar).
- Soporte para comprobaciones en lote paralelo usando curl_multi para acelerar, con control de concurrencia.
Resumen
Este enfoque proporciona una solución completa para auditar enlaces rotos en WordPress usando PHP: extracción fiable de enlaces, comprobación robusta (HEAD fallback GET), almacenamiento en tabla dedicada, tareas programadas y una interfaz administrativa para gestionar resultados. Implementar la solución con atención al rendimiento (procesamiento por lotes, WP-Cron vs WP-CLI, uso de background processing) y seguridad (capabilities, nonces, sanitización) permitirá mantener la calidad de los enlaces del sitio sin afectar la estabilidad del servidor.
Apéndice: funciones útiles rápidas
Función | Descripción |
---|---|
link_audit_extract_links_from_content() | Extrae enlaces de HTML mediante DOMDocument. |
link_audit_check_url() | Comprueba código HTTP de una URL con wp_remote_head y fallback a wp_remote_get. |
link_audit_save_result() | Inserta o actualiza resultado en la tabla de auditoría. |
link_audit_scan_posts_batch() | Procesa posts por lotes para evitar sobrecarga. |
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |