Como auditar enlaces rotos y reportar en admin con PHP en WordPress

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)

    echo 

Enlaces rotos detectados

echo echo foreach (rows as row) { post_title = row->post_id ? get_the_title(row->post_id) : edit_link = row->post_id ? get_edit_post_link(row->post_id) : row_url = esc_url(row->url) echo echo echo echo echo echo echo echo } echo
IDPostURLCódigoÚltimo chequeoAcciones
. intval(row->id) . . esc_html(post_title) . (edit_link ? [Editar] : ) . . esc_html(row->url) . . intval(row->http_code) . . esc_html(row->status_text) . . esc_html(row->last_checked) . echo id)) . >Recomprobar echo id)) . >Ignorar echo
// Acciones GET simples (en producción usar nonces y capacidades) if (isset(_GET[ignore])) { id = intval(_GET[ignore]) wpdb->update(table, array(ignored => 1), array(id => id)) 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) echo

Rechequeo 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 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *