fix(structure): Rename assets and inc folders for Linux compatibility

- assets → Assets
- inc → Inc

Completes the case-sensitivity fixes for Linux servers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-26 22:55:31 -06:00
parent 90863cd8f5
commit 33d17f4b56
44 changed files with 0 additions and 0 deletions

151
Inc/adsense-delay.php Normal file
View File

@@ -0,0 +1,151 @@
<?php
/**
* AdSense Delay Loading Functionality
*
* Delays the loading of AdSense scripts until user interaction or timeout
* to improve initial page load performance.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Retarda la carga de scripts de AdSense interceptando el buffer de salida
*
* Esta función inicia el output buffering y reemplaza los scripts de AdSense
* con versiones retrasadas cuando se renderiza la página.
*/
function roi_delay_adsense_scripts() {
// Solo ejecutar en frontend
if (is_admin()) {
return;
}
// Verificar si el retardo de AdSense está habilitado (Clean Architecture)
$is_enabled = roi_get_component_setting('adsense-delay', 'visibility', 'is_enabled', true);
if (!$is_enabled) {
return;
}
// Iniciar output buffering
ob_start('roi_replace_adsense_scripts');
}
add_action('template_redirect', 'roi_delay_adsense_scripts', 1);
/**
* Reemplaza scripts de AdSense con versiones retrasadas
*
* Esta función procesa la salida HTML y reemplaza las etiquetas de script
* estándar de AdSense con versiones de carga retrasada.
*
* @param string $html El contenido HTML a procesar
* @return string HTML modificado con scripts de AdSense retrasados
*/
function roi_replace_adsense_scripts($html) {
// Solo procesar si hay contenido real de AdSense
if (strpos($html, 'pagead2.googlesyndication.com') === false &&
strpos($html, 'adsbygoogle.js') === false) {
return $html;
}
// Patrones para encontrar etiquetas de script de AdSense
$patterns = array(
// Buscar etiquetas de script async para AdSense
'/<script\s+async\s+src=["\']https:\/\/pagead2\.googlesyndication\.com\/pagead\/js\/adsbygoogle\.js[^"\']*["\']\s*(?:crossorigin=["\']anonymous["\'])?\s*><\/script>/i',
// Buscar etiquetas de script sin async
'/<script\s+src=["\']https:\/\/pagead2\.googlesyndication\.com\/pagead\/js\/adsbygoogle\.js[^"\']*["\']\s*(?:crossorigin=["\']anonymous["\'])?\s*><\/script>/i',
// Buscar scripts inline de adsbygoogle.push
'/<script>\s*\(adsbygoogle\s*=\s*window\.adsbygoogle\s*\|\|\s*\[\]\)\.push\(\{[^}]*\}\);\s*<\/script>/is',
);
// Reemplazar scripts async de AdSense con versiones retrasadas
$replacements = array(
// Reemplazar etiqueta de script async con atributo data para carga retrasada
'<script type="text/plain" data-adsense-script src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" crossorigin="anonymous"></script>',
// Reemplazar etiqueta de script no-async
'<script type="text/plain" data-adsense-script src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" crossorigin="anonymous"></script>',
// Reemplazar scripts de push inline con versiones retrasadas
'<script type="text/plain" data-adsense-push>$0</script>',
);
// Primera pasada: reemplazar etiquetas de script
$html = preg_replace($patterns[0], $replacements[0], $html);
$html = preg_replace($patterns[1], $replacements[1], $html);
// Segunda pasada: reemplazar llamadas inline de push
$html = preg_replace_callback(
'/<script>\s*\(adsbygoogle\s*=\s*window\.adsbygoogle\s*\|\|\s*\[\]\)\.push\(\{[^}]*\}\);\s*<\/script>/is',
function($matches) {
return '<script type="text/plain" data-adsense-push>' . $matches[0] . '</script>';
},
$html
);
// Agregar comentario para indicar que se procesó (solo en modo debug)
if (defined('WP_DEBUG') && WP_DEBUG) {
$html = str_replace('</body>', '<!-- Scripts de AdSense retrasados por ROI Theme --></body>', $html);
}
return $html;
}
/**
* Agrega script inline para inicializar AdSense retrasado
*
* Esto agrega un pequeño script inline que marca AdSense como listo para cargar
* después de que adsense-loader.js ha sido enqueued.
*/
function roi_add_adsense_init_script() {
// Verificar si el retardo de AdSense está habilitado (Clean Architecture)
$is_enabled = roi_get_component_setting('adsense-delay', 'visibility', 'is_enabled', true);
if (!$is_enabled || is_admin()) {
return;
}
?>
<script>
// Inicializar flag de retardo de AdSense
window.roiAdsenseDelayed = true;
</script>
<?php
}
add_action('wp_head', 'roi_add_adsense_init_script', 1);
/**
* INSTRUCCIONES DE USO:
*
* Para activar el retardo de carga de AdSense:
* 1. Ir al panel de opciones del tema (Dashboard > ROI Theme Options)
* 2. En la sección "Performance", activar la opción "Delay AdSense Loading"
* 3. Guardar cambios
*
* Comportamiento:
* - Los scripts de AdSense NO se cargarán hasta que el usuario:
* * Haga scroll en la página
* * Haga click en cualquier parte
* * Toque la pantalla (móviles)
* * Mueva el mouse
* * Presione una tecla
* - Si no hay interacción, los scripts se cargarán después de 5 segundos
*
* Beneficios:
* - Mejora significativa en Core Web Vitals (FID, TBT)
* - Reduce el tiempo de carga inicial de la página
* - No afecta la monetización (los ads se siguen mostrando)
* - Sin layout shifts al cargar los ads
*
* Para desactivar:
* - Desmarcar la opción en el panel de opciones del tema
* - Los scripts de AdSense se cargarán normalmente
*/

316
Inc/apu-tables.php Normal file
View File

@@ -0,0 +1,316 @@
<?php
/**
* APU Tables Processing
* Funciones helper para tablas de Análisis de Precios Unitarios
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Procesa automáticamente tablas APU en el contenido
*
* Detecta tablas con el atributo data-apu y las envuelve
* con la clase .analisis para aplicar estilos específicos.
*
* @param string $content El contenido del post
* @return string El contenido procesado
*/
function roi_process_apu_tables($content) {
// Verificar que haya contenido
if (empty($content)) {
return $content;
}
// Patrón para detectar tablas con atributo data-apu
$pattern = '/<table([^>]*?)data-apu([^>]*?)>(.*?)<\/table>/is';
// Reemplazar cada tabla encontrada
$content = preg_replace_callback($pattern, function($matches) {
$before_attrs = $matches[1];
$after_attrs = $matches[2];
$table_content = $matches[3];
// Reconstruir la tabla sin el atributo data-apu
$table = '<table' . $before_attrs . $after_attrs . '>' . $table_content . '</table>';
// Envolver con div.analisis
return '<div class="analisis">' . $table . '</div>';
}, $content);
return $content;
}
add_filter('the_content', 'roi_process_apu_tables', 20);
/**
* Shortcode: [apu_table]
* Permite envolver tablas manualmente con la clase .analisis
*
* Uso:
* [apu_table]
* <table>
* <thead>...</thead>
* <tbody>...</tbody>
* </table>
* [/apu_table]
*
* @param array $atts Atributos del shortcode
* @param string $content Contenido del shortcode
* @return string HTML procesado
*/
function roi_apu_table_shortcode($atts, $content = null) {
// Verificar que haya contenido
if (empty($content)) {
return '';
}
// Procesar shortcodes anidados si los hay
$content = do_shortcode($content);
// Envolver con la clase .analisis
return '<div class="analisis">' . $content . '</div>';
}
add_shortcode('apu_table', 'roi_apu_table_shortcode');
/**
* Shortcode: [apu_row type="tipo"]
* Facilita la creación de filas especiales en tablas APU
*
* Tipos disponibles:
* - section: Encabezado de sección (Material, Mano de Obra, etc)
* - subtotal: Fila de subtotal
* - total: Fila de total final
*
* Uso:
* [apu_row type="section"]
* <td></td>
* <td>Material</td>
* <td class="c3"></td>
* <td class="c4"></td>
* <td class="c5"></td>
* <td class="c6"></td>
* [/apu_row]
*
* @param array $atts Atributos del shortcode
* @param string $content Contenido del shortcode
* @return string HTML procesado
*/
function roi_apu_row_shortcode($atts, $content = null) {
// Atributos por defecto
$atts = shortcode_atts(
array(
'type' => 'normal',
),
$atts,
'apu_row'
);
// Verificar que haya contenido
if (empty($content)) {
return '';
}
// Determinar la clase según el tipo
$class = '';
switch ($atts['type']) {
case 'section':
$class = 'section-header';
break;
case 'subtotal':
$class = 'subtotal-row';
break;
case 'total':
$class = 'total-row';
break;
default:
$class = '';
}
// Procesar shortcodes anidados
$content = do_shortcode($content);
// Construir la fila con la clase apropiada
if (!empty($class)) {
return '<tr class="' . esc_attr($class) . '">' . $content . '</tr>';
} else {
return '<tr>' . $content . '</tr>';
}
}
add_shortcode('apu_row', 'roi_apu_row_shortcode');
/**
* Función helper para generar una tabla APU completa
*
* Esta función puede ser llamada desde templates para generar
* tablas APU programáticamente.
*
* @param array $data Array con la estructura de la tabla
* @return string HTML de la tabla completa
*
* Ejemplo de estructura de datos:
* array(
* 'headers' => array('Clave', 'Descripción', 'Unidad', 'Cantidad', 'Costo', 'Importe'),
* 'sections' => array(
* array(
* 'title' => 'Material',
* 'rows' => array(
* array('AGRE-016', 'Agua potable', 'm3', '0.237500', '$19.14', '$4.55'),
* array('AGRE-001', 'Arena en camión de 6 m3', 'm3', '0.541500', '$1,750.00', '$947.63'),
* ),
* 'subtotal' => '$2,956.51'
* ),
* array(
* 'title' => 'Mano de Obra',
* 'rows' => array(
* array('MOCU-027', 'Cuadrilla No 27', 'jor', '0.100000', '$2,257.04', '$225.70'),
* ),
* 'subtotal' => '$225.70'
* ),
* ),
* 'total' => '$3,283.52'
* )
*/
function roi_generate_apu_table($data) {
// Validar datos mínimos
if (empty($data) || !isset($data['sections'])) {
return '';
}
// Iniciar buffer de salida
ob_start();
?>
<div class="analisis">
<table>
<?php if (isset($data['headers']) && is_array($data['headers'])): ?>
<thead>
<tr>
<?php foreach ($data['headers'] as $header): ?>
<th scope="col"><?php echo esc_html($header); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<?php endif; ?>
<tbody>
<?php foreach ($data['sections'] as $section): ?>
<?php if (isset($section['title'])): ?>
<!-- Encabezado de sección -->
<tr class="section-header">
<td></td>
<td><?php echo esc_html($section['title']); ?></td>
<td class="c3"></td>
<td class="c4"></td>
<td class="c5"></td>
<td class="c6"></td>
</tr>
<?php endif; ?>
<?php if (isset($section['rows']) && is_array($section['rows'])): ?>
<?php foreach ($section['rows'] as $row): ?>
<tr>
<?php foreach ($row as $index => $cell): ?>
<?php
// Aplicar clases especiales a columnas específicas
$class = '';
if ($index === 2) $class = ' class="c3"';
elseif ($index === 3) $class = ' class="c4"';
elseif ($index === 4) $class = ' class="c5"';
elseif ($index === 5) $class = ' class="c6"';
?>
<td<?php echo $class; ?>><?php echo esc_html($cell); ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
<?php endif; ?>
<?php if (isset($section['subtotal'])): ?>
<!-- Subtotal de sección -->
<tr class="subtotal-row">
<td></td>
<td>Suma de <?php echo esc_html($section['title']); ?></td>
<td class="c3"></td>
<td class="c4"></td>
<td class="c5"></td>
<td class="c6"><?php echo esc_html($section['subtotal']); ?></td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
<?php if (isset($data['total'])): ?>
<!-- Total final -->
<tr class="total-row">
<td></td>
<td>Costo Directo</td>
<td class="c3"></td>
<td class="c4"></td>
<td class="c5"></td>
<td class="c6"><?php echo esc_html($data['total']); ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
return ob_get_clean();
}
/**
* Agregar clases CSS al body cuando hay tablas APU
*
* Esto permite aplicar estilos adicionales a nivel de página
* cuando se detectan tablas APU.
*
* @param array $classes Array de clases del body
* @return array Array modificado de clases
*/
function roi_add_apu_body_class($classes) {
// Solo en posts individuales
if (is_single()) {
global $post;
// Verificar si el contenido tiene tablas APU
if (has_shortcode($post->post_content, 'apu_table') ||
strpos($post->post_content, 'data-apu') !== false ||
strpos($post->post_content, 'class="analisis"') !== false) {
$classes[] = 'has-apu-tables';
}
}
return $classes;
}
add_filter('body_class', 'roi_add_apu_body_class');
/**
* Permitir ciertos atributos HTML en tablas para el editor
*
* Esto asegura que los atributos data-apu y las clases especiales
* no sean eliminados por el sanitizador de WordPress.
*
* @param array $allowed_tags Array de tags HTML permitidos
* @param string $context Contexto de uso
* @return array Array modificado de tags permitidos
*/
function roi_allow_apu_table_attributes($allowed_tags, $context) {
if ($context === 'post') {
// Permitir atributo data-apu en tablas
if (isset($allowed_tags['table'])) {
$allowed_tags['table']['data-apu'] = true;
}
// Asegurar que las clases específicas estén permitidas
if (isset($allowed_tags['tr'])) {
$allowed_tags['tr']['class'] = true;
}
if (isset($allowed_tags['td'])) {
$allowed_tags['td']['class'] = true;
}
}
return $allowed_tags;
}
add_filter('wp_kses_allowed_html', 'roi_allow_apu_table_attributes', 10, 2);

81
Inc/category-badge.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
/**
* Category Badge Functions
*
* Funciones para mostrar badge de categoría sobre el H1 en single posts.
* Utiliza clases de Bootstrap para el estilo del badge.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Salir si se accede directamente
if (!defined('ABSPATH')) {
exit;
}
/**
* Obtiene el HTML del badge de categoría
*
* Retorna el HTML del badge con la primera categoría del post,
* excluyendo "Uncategorized" y "Sin categoría".
* Utiliza clases de Bootstrap para el estilo.
*
* @return string HTML del badge de categoría o string vacío
*/
function roi_get_category_badge() {
// Verificar si la función está habilitada (Clean Architecture)
$is_enabled = roi_get_component_setting('category-badge', 'visibility', 'is_enabled', true);
if (!$is_enabled) {
return '';
}
// Solo mostrar en single posts (no en páginas ni archives)
if (!is_single()) {
return '';
}
// Obtener todas las categorías del post actual
$categories = get_the_category();
// Si no hay categorías, retornar vacío
if (empty($categories)) {
return '';
}
// Filtrar categorías para excluir "Uncategorized" o "Sin categoría"
$filtered_categories = array_filter($categories, function($category) {
$excluded_slugs = array('uncategorized', 'sin-categoria');
return !in_array($category->slug, $excluded_slugs);
});
// Si después del filtro no quedan categorías, retornar vacío
if (empty($filtered_categories)) {
return '';
}
// Tomar la primera categoría (principal)
$category = reset($filtered_categories);
// Generar HTML del badge con clases Bootstrap
// Utiliza badge bg-primary de Bootstrap 5
$output = sprintf(
'<div class="category-badge mb-3"><a href="%s" class="badge bg-primary text-decoration-none" rel="category tag">%s</a></div>',
esc_url(get_category_link($category->term_id)),
esc_html($category->name)
);
return $output;
}
/**
* Muestra el badge de categoría
*
* Template tag para imprimir directamente el badge de categoría.
* Uso en templates: <?php roi_display_category_badge(); ?>
*
* @return void
*/
function roi_display_category_badge() {
echo roi_get_category_badge();
}

253
Inc/comments-disable.php Normal file
View File

@@ -0,0 +1,253 @@
<?php
/**
* Desactivar completamente el sistema de comentarios
*
* Este archivo desactiva completamente los comentarios en WordPress,
* tanto en el frontend como en el área de administración.
*
* @package ROI_Theme
* @since 1.0.0
* @link https://github.com/prime-leads-app/analisisdepreciosunitarios.com/issues/4
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Desactivar soporte de comentarios y pingbacks
*
* Cierra comentarios y pingbacks para todos los post types.
*
* @since 1.0.0
* @param bool $open Si los comentarios están abiertos o no.
* @return bool Siempre retorna false.
*/
function roi_disable_comments_status() {
return false;
}
add_filter('comments_open', 'roi_disable_comments_status', 20, 2);
add_filter('pings_open', 'roi_disable_comments_status', 20, 2);
/**
* Ocultar comentarios existentes
*
* Retorna un array vacío para ocultar cualquier comentario existente.
*
* @since 1.0.0
* @param array $comments Array de comentarios.
* @return array Array vacío.
*/
function roi_hide_existing_comments($comments) {
return array();
}
add_filter('comments_array', 'roi_hide_existing_comments', 10, 2);
/**
* Desactivar feeds de comentarios
*
* Remueve los enlaces de feeds de comentarios del head.
*
* @since 1.0.0
*/
function roi_disable_comment_feeds() {
// Remover enlaces de feeds de comentarios
remove_action('wp_head', 'feed_links_extra', 3);
// Desactivar feeds de comentarios
add_action('do_feed_rss2_comments', 'roi_disable_feed_comments');
add_action('do_feed_atom_comments', 'roi_disable_feed_comments');
}
add_action('init', 'roi_disable_comment_feeds');
/**
* Retornar error en feeds de comentarios
*
* @since 1.0.0
*/
function roi_disable_feed_comments() {
wp_die(
esc_html__('Los comentarios están desactivados en este sitio.', 'roi-theme'),
esc_html__('Comentarios no disponibles', 'roi-theme'),
array(
'response' => 404,
'back_link' => true,
)
);
}
/**
* Desactivar script de respuesta de comentarios
*
* Remueve el script comment-reply.js del frontend.
*
* @since 1.0.0
*/
function roi_disable_comment_reply_script() {
wp_deregister_script('comment-reply');
}
add_action('wp_enqueue_scripts', 'roi_disable_comment_reply_script', 100);
/**
* Remover menú de comentarios del admin
*
* Oculta el menú "Comentarios" del área de administración.
*
* @since 1.0.0
*/
function roi_remove_comments_admin_menu() {
remove_menu_page('edit-comments.php');
}
add_action('admin_menu', 'roi_remove_comments_admin_menu');
/**
* Remover comentarios de la admin bar
*
* Oculta el icono de comentarios de la barra de administración.
*
* @since 1.0.0
* @param WP_Admin_Bar $wp_admin_bar Instancia de WP_Admin_Bar.
*/
function roi_remove_comments_admin_bar($wp_admin_bar) {
$wp_admin_bar->remove_menu('comments');
}
add_action('admin_bar_menu', 'roi_remove_comments_admin_bar', 60);
/**
* Remover metabox de comentarios del editor
*
* Oculta el metabox de comentarios en el editor de posts y páginas.
*
* @since 1.0.0
*/
function roi_remove_comments_metabox() {
// Post types por defecto
remove_meta_box('commentstatusdiv', 'post', 'normal');
remove_meta_box('commentstatusdiv', 'page', 'normal');
remove_meta_box('commentsdiv', 'post', 'normal');
remove_meta_box('commentsdiv', 'page', 'normal');
remove_meta_box('trackbacksdiv', 'post', 'normal');
remove_meta_box('trackbacksdiv', 'page', 'normal');
// Aplicar a cualquier custom post type que pueda existir
$post_types = get_post_types(array('public' => true), 'names');
foreach ($post_types as $post_type) {
if (post_type_supports($post_type, 'comments')) {
remove_post_type_support($post_type, 'comments');
remove_post_type_support($post_type, 'trackbacks');
}
}
}
add_action('admin_init', 'roi_remove_comments_metabox');
/**
* Ocultar columna de comentarios en listados del admin
*
* Remueve la columna de comentarios de los listados de posts/páginas.
*
* @since 1.0.0
* @param array $columns Columnas actuales.
* @return array Columnas modificadas sin comentarios.
*/
function roi_remove_comments_column($columns) {
unset($columns['comments']);
return $columns;
}
// Aplicar a posts y páginas
add_filter('manage_posts_columns', 'roi_remove_comments_column');
add_filter('manage_pages_columns', 'roi_remove_comments_column');
/**
* Desactivar widgets de comentarios
*
* Remueve los widgets relacionados con comentarios.
*
* @since 1.0.0
*/
function roi_disable_comments_widgets() {
unregister_widget('WP_Widget_Recent_Comments');
}
add_action('widgets_init', 'roi_disable_comments_widgets');
/**
* Remover estilos CSS de comentarios recientes
*
* Remueve los estilos inline del widget de comentarios recientes.
*
* @since 1.0.0
*/
function roi_remove_recent_comments_style() {
global $wp_widget_factory;
if (isset($wp_widget_factory->widgets['WP_Widget_Recent_Comments'])) {
remove_action('wp_head', array(
$wp_widget_factory->widgets['WP_Widget_Recent_Comments'],
'recent_comments_style'
));
}
}
add_action('widgets_init', 'roi_remove_recent_comments_style');
/**
* Redireccionar URLs de comentarios (opcional)
*
* Si alguien intenta acceder directamente a URLs de comentarios,
* redirigir al post padre.
*
* @since 1.0.0
*/
function roi_redirect_comment_urls() {
if (is_comment_feed()) {
wp_safe_redirect(home_url(), 301);
exit;
}
}
add_action('template_redirect', 'roi_redirect_comment_urls');
/**
* Prevenir nuevos comentarios via REST API
*
* Desactiva endpoints de comentarios en REST API.
*
* @since 1.0.0
* @param array $endpoints Endpoints disponibles.
* @return array Endpoints sin comentarios.
*/
function roi_disable_comments_rest_api($endpoints) {
if (isset($endpoints['/wp/v2/comments'])) {
unset($endpoints['/wp/v2/comments']);
}
if (isset($endpoints['/wp/v2/comments/(?P<id>[\d]+)'])) {
unset($endpoints['/wp/v2/comments/(?P<id>[\d]+)']);
}
return $endpoints;
}
add_filter('rest_endpoints', 'roi_disable_comments_rest_api');
/**
* Ocultar opciones de comentarios en el dashboard
*
* Remueve metaboxes de comentarios del dashboard.
*
* @since 1.0.0
*/
function roi_remove_dashboard_comments() {
remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal');
}
add_action('admin_init', 'roi_remove_dashboard_comments');
/**
* Desactivar notificaciones de comentarios
*
* Previene el envío de emails de notificación de comentarios.
*
* @since 1.0.0
* @return bool Siempre retorna false.
*/
function roi_disable_comment_emails() {
return false;
}
add_filter('notify_post_author', 'roi_disable_comment_emails', 10, 2);
add_filter('notify_moderator', 'roi_disable_comment_emails', 10, 2);

367
Inc/critical-css.php Normal file
View File

@@ -0,0 +1,367 @@
<?php
/**
* Critical CSS Generator and Inline Loader
*
* This file provides functionality to inline critical CSS for above-the-fold content,
* improving First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
*
* IMPORTANT: This feature is DISABLED by default. Enable it via Customizer.
*
* How it works:
* 1. When enabled, critical CSS is inlined in the <head>
* 2. Main stylesheet is loaded asynchronously after page load
* 3. Improves Core Web Vitals by reducing render-blocking CSS
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Check if Critical CSS is enabled
*
* @since 1.0.0
* @return bool
*/
function roi_is_critical_css_enabled() {
return get_theme_mod( 'roi_enable_critical_css', false );
}
/**
* Get Critical CSS Content
*
* Returns the critical CSS for the current page type.
* You can customize this based on page types (home, single, archive, etc.)
*
* @since 1.0.0
* @return string Critical CSS content
*/
function roi_get_critical_css() {
// Define critical CSS based on page type
$critical_css = '';
// Get transient to cache critical CSS
$transient_key = 'roi_critical_css_' . roi_get_page_type();
$cached_css = get_transient( $transient_key );
if ( false !== $cached_css ) {
return $cached_css;
}
// Generate critical CSS based on page type
if ( is_front_page() || is_home() ) {
$critical_css = roi_get_home_critical_css();
} elseif ( is_single() ) {
$critical_css = roi_get_single_critical_css();
} elseif ( is_archive() || is_category() || is_tag() ) {
$critical_css = roi_get_archive_critical_css();
} else {
$critical_css = roi_get_default_critical_css();
}
// Cache for 24 hours
set_transient( $transient_key, $critical_css, DAY_IN_SECONDS );
return $critical_css;
}
/**
* Get current page type for caching
*
* @since 1.0.0
* @return string Page type identifier
*/
function roi_get_page_type() {
if ( is_front_page() ) {
return 'home';
} elseif ( is_single() ) {
return 'single';
} elseif ( is_archive() ) {
return 'archive';
} elseif ( is_search() ) {
return 'search';
} elseif ( is_404() ) {
return '404';
} else {
return 'page';
}
}
/**
* Critical CSS for Homepage
*
* @since 1.0.0
* @return string
*/
function roi_get_home_critical_css() {
return '
/* Reset and Base */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;line-height:1.6;color:#333;background:#fff}
img{max-width:100%;height:auto;display:block}
a{color:#0066cc;text-decoration:none}
/* Header */
.site-header{background:#fff;border-bottom:1px solid #e5e5e5;position:sticky;top:0;z-index:1000}
.site-header .container{max-width:1200px;margin:0 auto;padding:1rem}
.site-branding{display:flex;align-items:center}
.site-title{margin:0;font-size:1.5rem;font-weight:700}
/* Hero Section */
.hero-section{padding:3rem 1rem;text-align:center;background:#f8f9fa}
.hero-title{font-size:2.5rem;margin:0 0 1rem;font-weight:700;line-height:1.2}
.hero-description{font-size:1.125rem;color:#666;max-width:600px;margin:0 auto}
/* Container */
.container{max-width:1200px;margin:0 auto;padding:0 1rem}
/* Featured Posts Grid */
.featured-posts{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:2rem;margin:2rem 0}
.post-card{background:#fff;border:1px solid #e5e5e5;border-radius:8px;overflow:hidden;transition:transform .2s}
.post-card:hover{transform:translateY(-4px)}
/* Skip to content */
.skip-link{position:absolute;left:-999px;top:0;background:#0066cc;color:#fff;padding:.5rem 1rem;text-decoration:none}
.skip-link:focus{left:0}
';
}
/**
* Critical CSS for Single Post/Page
*
* @since 1.0.0
* @return string
*/
function roi_get_single_critical_css() {
return '
/* Reset and Base */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;line-height:1.6;color:#333;background:#fff}
img{max-width:100%;height:auto;display:block}
a{color:#0066cc;text-decoration:none}
h1,h2,h3,h4,h5,h6{margin:1.5rem 0 1rem;font-weight:700;line-height:1.3}
h1{font-size:2.5rem}
h2{font-size:2rem}
h3{font-size:1.5rem}
p{margin:0 0 1.5rem}
/* Header */
.site-header{background:#fff;border-bottom:1px solid #e5e5e5;position:sticky;top:0;z-index:1000}
.site-header .container{max-width:1200px;margin:0 auto;padding:1rem}
/* Article */
.entry-header{margin:2rem 0}
.entry-title{font-size:2.5rem;margin:0 0 1rem;line-height:1.2}
.entry-meta{color:#666;font-size:.875rem}
.entry-content{max-width:800px;margin:0 auto;font-size:1.125rem;line-height:1.8}
.featured-image{margin:2rem 0}
/* Container */
.container{max-width:1200px;margin:0 auto;padding:0 1rem}
/* Skip to content */
.skip-link{position:absolute;left:-999px;top:0;background:#0066cc;color:#fff;padding:.5rem 1rem;text-decoration:none}
.skip-link:focus{left:0}
';
}
/**
* Critical CSS for Archive Pages
*
* @since 1.0.0
* @return string
*/
function roi_get_archive_critical_css() {
return '
/* Reset and Base */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;line-height:1.6;color:#333;background:#fff}
img{max-width:100%;height:auto;display:block}
a{color:#0066cc;text-decoration:none}
/* Header */
.site-header{background:#fff;border-bottom:1px solid #e5e5e5;position:sticky;top:0;z-index:1000}
.site-header .container{max-width:1200px;margin:0 auto;padding:1rem}
/* Archive Header */
.archive-header{padding:2rem 1rem;background:#f8f9fa;border-bottom:1px solid #e5e5e5}
.archive-title{margin:0;font-size:2rem;font-weight:700}
.archive-description{margin:1rem 0 0;color:#666;font-size:1.125rem}
/* Posts Grid */
.posts-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:2rem;margin:2rem 0}
.post-card{background:#fff;border:1px solid #e5e5e5;border-radius:8px;overflow:hidden}
/* Container */
.container{max-width:1200px;margin:0 auto;padding:0 1rem}
/* Skip to content */
.skip-link{position:absolute;left:-999px;top:0;background:#0066cc;color:#fff;padding:.5rem 1rem;text-decoration:none}
.skip-link:focus{left:0}
';
}
/**
* Critical CSS for Default Pages
*
* @since 1.0.0
* @return string
*/
function roi_get_default_critical_css() {
return '
/* Reset and Base */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;line-height:1.6;color:#333;background:#fff}
img{max-width:100%;height:auto;display:block}
a{color:#0066cc;text-decoration:none}
/* Header */
.site-header{background:#fff;border-bottom:1px solid #e5e5e5;position:sticky;top:0;z-index:1000}
.site-header .container{max-width:1200px;margin:0 auto;padding:1rem}
/* Container */
.container{max-width:1200px;margin:0 auto;padding:0 1rem}
/* Content */
.site-content{min-height:50vh;padding:2rem 0}
/* Skip to content */
.skip-link{position:absolute;left:-999px;top:0;background:#0066cc;color:#fff;padding:.5rem 1rem;text-decoration:none}
.skip-link:focus{left:0}
';
}
/**
* Output Critical CSS inline in head
*
* @since 1.0.0
*/
function roi_output_critical_css() {
if ( ! roi_is_critical_css_enabled() ) {
return;
}
$critical_css = roi_get_critical_css();
if ( empty( $critical_css ) ) {
return;
}
// Minify CSS (remove extra whitespace and newlines)
$critical_css = preg_replace( '/\s+/', ' ', $critical_css );
$critical_css = trim( $critical_css );
// Output inline critical CSS
echo '<style id="roi-critical-css">' . $critical_css . '</style>' . "\n";
}
add_action( 'wp_head', 'roi_output_critical_css', 1 );
/**
* Load main stylesheet asynchronously when critical CSS is enabled
*
* @since 1.0.0
*/
function roi_async_main_stylesheet() {
if ( ! roi_is_critical_css_enabled() ) {
return;
}
// Dequeue main stylesheet to prevent render-blocking
wp_dequeue_style( 'roi-theme-style' );
// Enqueue with media="print" and onload to load asynchronously
wp_enqueue_style(
'roi-theme-style-async',
get_stylesheet_uri(),
array(),
ROI_VERSION,
'print'
);
// Add onload attribute to switch media to "all"
add_filter( 'style_loader_tag', 'roi_add_async_attribute', 10, 2 );
}
add_action( 'wp_enqueue_scripts', 'roi_async_main_stylesheet', 999 );
/**
* Add async loading attributes to stylesheet
*
* @since 1.0.0
* @param string $html The link tag for the enqueued style.
* @param string $handle The style's registered handle.
* @return string Modified link tag
*/
function roi_add_async_attribute( $html, $handle ) {
if ( 'roi-theme-style-async' !== $handle ) {
return $html;
}
// Add onload attribute to switch media to "all"
$html = str_replace(
"media='print'",
"media='print' onload=\"this.media='all'\"",
$html
);
// Add noscript fallback
$html .= '<noscript><link rel="stylesheet" href="' . get_stylesheet_uri() . '"></noscript>';
return $html;
}
/**
* Add Customizer setting for Critical CSS
*
* @since 1.0.0
* @param WP_Customize_Manager $wp_customize Theme Customizer object.
*/
function roi_critical_css_customizer( $wp_customize ) {
// Add Performance section
$wp_customize->add_section(
'roi_performance',
array(
'title' => __( 'Performance Optimization', 'roi-theme' ),
'priority' => 130,
)
);
// Critical CSS Enable/Disable
$wp_customize->add_setting(
'roi_enable_critical_css',
array(
'default' => false,
'sanitize_callback' => 'roi_sanitize_checkbox',
'transport' => 'refresh',
)
);
$wp_customize->add_control(
'roi_enable_critical_css',
array(
'label' => __( 'Enable Critical CSS', 'roi-theme' ),
'description' => __( 'Inline critical CSS and load main stylesheet asynchronously. This can improve Core Web Vitals but may cause a flash of unstyled content (FOUC). Test thoroughly before enabling in production.', 'roi-theme' ),
'section' => 'roi_performance',
'type' => 'checkbox',
)
);
}
add_action( 'customize_register', 'roi_critical_css_customizer' );
/**
* Clear critical CSS cache when theme is updated
*
* @since 1.0.0
*/
function roi_clear_critical_css_cache() {
$page_types = array( 'home', 'single', 'archive', 'search', '404', 'page' );
foreach ( $page_types as $type ) {
delete_transient( 'roi_critical_css_' . $type );
}
}
add_action( 'after_switch_theme', 'roi_clear_critical_css_cache' );
add_action( 'customize_save_after', 'roi_clear_critical_css_cache' );

511
Inc/enqueue-scripts.php Normal file
View File

@@ -0,0 +1,511 @@
<?php
/**
* Enqueue Bootstrap 5 and Custom Scripts
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Enqueue typography system
*
* SELF-HOSTED: Fuentes Poppins ahora se cargan desde Assets/fonts/
* Eliminada dependencia de Google Fonts para:
* - Mejorar rendimiento (sin requests externos)
* - Cumplimiento GDPR (sin tracking de Google)
* - Evitar bloqueo de renderizado
*/
function roi_enqueue_fonts() {
// Fonts CSS local con @font-face para Poppins self-hosted
wp_enqueue_style(
'roi-fonts',
get_template_directory_uri() . '/Assets/css/css-global-fonts.css',
array(),
'1.1.0', // Bump version: self-hosted fonts
'all'
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_fonts', 1);
/**
* Enqueue Bootstrap 5 styles and scripts
*/
function roi_enqueue_bootstrap() {
// Bootstrap CSS - with high priority
wp_enqueue_style(
'roi-bootstrap',
get_template_directory_uri() . '/Assets/vendor/bootstrap/css/bootstrap.min.css',
array('roi-fonts'),
'5.3.2',
'all'
);
// Bootstrap Icons CSS - LOCAL (Issue #135: CORS bloqueaba CDN)
wp_enqueue_style(
'bootstrap-icons',
get_template_directory_uri() . '/Assets/vendor/bootstrap-icons.min.css',
array('roi-bootstrap'),
'1.11.3',
'all'
);
// Variables CSS del Template RDash (NIVEL 1 - BLOQUEANTE - Issue #48)
wp_enqueue_style(
'roi-variables',
get_template_directory_uri() . '/Assets/css/css-global-variables.css',
array('roi-bootstrap'),
ROI_VERSION,
'all'
);
// Bootstrap JS Bundle - in footer with defer
wp_enqueue_script(
'roi-bootstrap-js',
get_template_directory_uri() . '/Assets/vendor/bootstrap/js/bootstrap.bundle.min.js',
array(),
'5.3.2',
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
// Dequeue jQuery if it was enqueued
wp_dequeue_script('jquery');
wp_deregister_script('jquery');
}
add_action('wp_enqueue_scripts', 'roi_enqueue_bootstrap', 5);
/**
* Enqueue main theme stylesheet
* FASE 1 - Este es el archivo CSS principal del tema
*/
function roi_enqueue_main_stylesheet() {
wp_enqueue_style(
'roi-main-style',
get_template_directory_uri() . '/Assets/css/style.css',
array('roi-variables'),
'1.0.5', // Arquitectura: Separación de responsabilidades CSS
'all'
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_main_stylesheet', 5);
/**
* Enqueue FASE 2 CSS - Template RDash Component Styles (Issues #58-64)
*
* Estilos que replican componentes del template RDash
*
* NOTA: Hero Section, Post Content y Related Posts ahora usan
* estilos generados dinámicamente desde sus Renderers.
*/
function roi_enqueue_fase2_styles() {
// Hero Section CSS - DESHABILITADO: estilos generados por HeroRenderer
// @see Public/Hero/Infrastructure/Ui/HeroRenderer.php
// Category Badges CSS - Clase genérica (Issue #62)
wp_enqueue_style(
'roi-badges',
get_template_directory_uri() . '/Assets/css/css-global-badges.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/Assets/css/css-global-badges.css'),
'all'
);
// Pagination CSS - Estilos personalizados (Issue #64)
wp_enqueue_style(
'roi-pagination',
get_template_directory_uri() . '/Assets/css/css-global-pagination.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/Assets/css/css-global-pagination.css'),
'all'
);
// Post Content Typography y Related Posts - DESHABILITADOS
// Los estilos ahora están integrados en style.css o generados dinámicamente
}
add_action('wp_enqueue_scripts', 'roi_enqueue_fase2_styles', 6);
/**
* Enqueue Global Components CSS
*
* ARQUITECTURA: Componentes globales que aparecen en todas las páginas
* Issue #121 - Separación de componentes del style.css
*
* @since 1.0.7
*/
function roi_enqueue_global_components() {
// Notification Bar CSS - DESHABILITADO: Los estilos de la barra de notificación ahora se generan
// dinámicamente desde el TopNotificationBarRenderer basado en los valores de la BD.
// @see Public/TopNotificationBar/Infrastructure/Ui/TopNotificationBarRenderer.php
/*
wp_enqueue_style(
'roi-notification-bar',
get_template_directory_uri() . '/Assets/css/componente-top-bar.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/Assets/css/componente-top-bar.css'),
'all'
);
*/
// Navbar CSS - DESHABILITADO: Los estilos del navbar ahora se generan
// dinámicamente desde el NavbarRenderer basado en los valores de la BD.
// El archivo componente-navbar.css tenía !important que sobrescribía
// los estilos configurados por el usuario en el Admin Panel.
// @see Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
/*
wp_enqueue_style(
'roi-navbar',
get_template_directory_uri() . '/Assets/css/componente-navbar.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/Assets/css/componente-navbar.css'),
'all'
);
*/
// Buttons CSS - DESHABILITADO: Los estilos del botón Let's Talk ahora se generan
// dinámicamente desde el CtaLetsTalkRenderer basado en los valores de la BD.
// El archivo componente-boton-lets-talk.css tenía !important que sobrescribía
// los estilos configurados por el usuario en el Admin Panel.
// @see Public/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkRenderer.php
/*
wp_enqueue_style(
'roi-buttons',
get_template_directory_uri() . '/Assets/css/componente-boton-lets-talk.css',
array('roi-bootstrap'),
filemtime(get_template_directory() . '/Assets/css/componente-boton-lets-talk.css'),
'all'
);
*/
}
add_action('wp_enqueue_scripts', 'roi_enqueue_global_components', 7);
/**
* Enqueue header scripts
*
* NOTA: CSS del header se genera dinámicamente desde NavbarRenderer
* @see Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
*/
function roi_enqueue_header() {
// Header JS - with defer strategy
wp_enqueue_script(
'roi-header-js',
get_template_directory_uri() . '/Assets/js/header.js',
array(),
'1.0.0',
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_header', 10);
/**
* Enqueue generic tables styles
*
* ARQUITECTURA: Estilos para tablas genéricas en post-content
* Solo en posts individuales
*/
function roi_enqueue_generic_tables() {
// Only enqueue on single posts
if (!is_single()) {
return;
}
// Generic Tables CSS
wp_enqueue_style(
'roi-generic-tables',
get_template_directory_uri() . '/Assets/css/css-global-generic-tables.css',
array('roi-bootstrap'),
ROI_VERSION,
'all'
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_generic_tables', 11);
/**
* Enqueue video iframe styles
*
* ARQUITECTURA: Estilos para videos embebidos (YouTube, Vimeo)
* Solo en posts individuales
*/
function roi_enqueue_video_styles() {
// Only enqueue on single posts
if (!is_single()) {
return;
}
// Video CSS
wp_enqueue_style(
'roi-video',
get_template_directory_uri() . '/Assets/css/css-global-video.css',
array('roi-bootstrap'),
ROI_VERSION,
'all'
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_video_styles', 11);
/**
* Enqueue main JavaScript
*
* ELIMINADO: custom-style.css (Issue #131)
* Motivo: Archivo contenía 95% código duplicado de style.css y otros componentes
* Código único movido a: generic-tables.css, video.css
*/
function roi_enqueue_main_javascript() {
// Main JavaScript - navbar scroll effects and interactions
wp_enqueue_script(
'roi-main-js',
get_template_directory_uri() . '/Assets/js/main.js',
array('roi-bootstrap-js'),
'1.0.3', // Cache bust: force remove defer with filter
true // Load in footer
);
// Localize script to pass theme URL to JavaScript
wp_localize_script(
'roi-main-js',
'roiheme',
array(
'themeUrl' => get_template_directory_uri(),
'ajaxUrl' => admin_url('admin-ajax.php'),
)
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_main_javascript', 11);
/**
* Remove defer from roi-main-js to make wp_localize_script work
* WordPress 6.3+ adds defer automatically to footer scripts with dependencies
* but wp_localize_script requires synchronous execution
*/
function roi_remove_defer_from_main_js($tag, $handle) {
if ('roi-main-js' === $handle) {
// Remove defer and data-wp-strategy attributes
$tag = str_replace(' defer', '', $tag);
$tag = str_replace(' data-wp-strategy="defer"', '', $tag);
}
return $tag;
}
add_filter('script_loader_tag', 'roi_remove_defer_from_main_js', 20, 2);
/**
* ELIMINADO: roi_enqueue_footer_styles
* Motivo: footer.css NO está documentado - CSS debe estar en style.css
* Ver: theme-documentation/16-componente-footer-contact-form/CSS-ESPECIFICO.md
*/
/**
* Enqueue accessibility styles and scripts
*/
function roi_enqueue_accessibility() {
// Accessibility CSS
wp_enqueue_style(
'roi-accessibility',
get_template_directory_uri() . '/Assets/css/css-global-accessibility.css',
array('roi-theme-style'),
ROI_VERSION,
'all'
);
// Accessibility JavaScript
wp_enqueue_script(
'roi-accessibility-js',
get_template_directory_uri() . '/Assets/js/accessibility.js',
array('roi-bootstrap-js'),
ROI_VERSION,
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_accessibility', 15);
/**
* Enqueue del script de carga retrasada de AdSense
*
* Este script se encarga de detectar la primera interacción del usuario
* (scroll, click, touch, etc.) y cargar los scripts de AdSense solo
* en ese momento, mejorando significativamente el rendimiento inicial.
*/
function roi_enqueue_adsense_loader() {
// Solo ejecutar en frontend
if (is_admin()) {
return;
}
// Verificar si el retardo de AdSense está habilitado (Clean Architecture)
$is_enabled = roi_get_component_setting('adsense-delay', 'visibility', 'is_enabled', true);
if (!$is_enabled) {
return;
}
// Enqueue del script de carga de AdSense
wp_enqueue_script(
'roi-adsense-loader',
get_template_directory_uri() . '/Assets/js/adsense-loader.js',
array(),
ROI_VERSION,
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_adsense_loader', 10);
/**
* Enqueue theme core styles
*
* ELIMINADO: theme.css (Issue #125)
* Motivo: theme.css contenía código experimental y sobrescrituras Bootstrap no documentadas
* Resultado: 638 líneas eliminadas - TODO el CSS documentado va en style.css
* Fecha: 2025-01-07
*/
function roi_enqueue_theme_styles() {
// Theme Core Styles - ELIMINADO theme.css
// wp_enqueue_style(
// 'roi-theme',
// get_template_directory_uri() . '/Assets/css/theme.css',
// array('roi-bootstrap'),
// '1.0.0',
// 'all'
// );
// Theme Animations
wp_enqueue_style(
'roi-animations',
get_template_directory_uri() . '/Assets/css/css-global-animations.css',
array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap'
'1.0.0',
'all'
);
// Theme Responsive Styles
wp_enqueue_style(
'roi-responsive',
get_template_directory_uri() . '/Assets/css/css-global-responsive.css',
array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap'
'1.0.0',
'all'
);
// Theme Utilities
wp_enqueue_style(
'roi-utilities',
get_template_directory_uri() . '/Assets/css/css-global-utilities.css',
array('roi-bootstrap'), // Cambiado de 'roi-theme' a 'roi-bootstrap'
'1.0.0',
'all'
);
// Print Styles
wp_enqueue_style(
'roi-print',
get_template_directory_uri() . '/Assets/css/css-global-print.css',
array(),
'1.0.0',
'print'
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_theme_styles', 13);
/**
* Enqueue social share styles
*
* DESHABILITADO: Los estilos de Social Share ahora se generan
* dinámicamente desde SocialShareRenderer basado en valores de BD.
* @see Public/SocialShare/Infrastructure/Ui/SocialShareRenderer.php
*/
// function roi_enqueue_social_share_styles() - REMOVED
/**
* Enqueue APU Tables styles
*/
function roi_enqueue_apu_tables_styles() {
// APU Tables CSS
wp_enqueue_style(
'roi-tables-apu',
get_template_directory_uri() . '/Assets/css/css-tablas-apu.css',
array('roi-bootstrap'),
ROI_VERSION,
'all'
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_apu_tables_styles', 15);
/**
* Enqueue APU Tables auto-class JavaScript
*
* Este script detecta automáticamente filas especiales en tablas .desglose y .analisis
* y les agrega las clases CSS correspondientes (section-header, subtotal-row, total-row)
*
* Issue #132
*/
function roi_enqueue_apu_tables_autoclass_script() {
// APU Tables Auto-Class JS
wp_enqueue_script(
'roi-apu-tables-autoclass',
get_template_directory_uri() . '/Assets/js/apu-tables-auto-class.js',
array(),
ROI_VERSION,
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'roi_enqueue_apu_tables_autoclass_script', 15);
/**
* Enqueue CTA Box Sidebar styles (Issue #36)
*
* DESHABILITADO: Los estilos del CTA Box Sidebar ahora se generan
* dinámicamente desde CtaBoxSidebarRenderer basado en valores de BD.
* @see Public/CtaBoxSidebar/Infrastructure/Ui/CtaBoxSidebarRenderer.php
*/
// function roi_enqueue_cta_box_sidebar_assets() - REMOVED
/**
* Enqueue TOC Sidebar styles (only on single posts)
*
* DESHABILITADO: Los estilos del TOC ahora se generan
* dinámicamente desde TableOfContentsRenderer basado en valores de BD.
* @see Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php
*
* @since 1.0.5
*/
// function roi_enqueue_toc_sidebar_assets() - REMOVED
/**
* Enqueue Footer Contact Form styles
*
* DESHABILITADO: Los estilos del Contact Form ahora se generan
* dinámicamente desde ContactFormRenderer basado en valores de BD.
* @see Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php
*/
// function roi_enqueue_footer_contact_assets() - REMOVED

434
Inc/featured-image.php Normal file
View File

@@ -0,0 +1,434 @@
<?php
/**
* Featured Image Functions
*
* Funciones para manejo de imágenes destacadas con comportamiento configurable.
* Sin placeholders - solo muestra imagen si existe.
* Issue #10 - Imágenes destacadas configurables
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Obtiene la imagen destacada de un post con configuración respetada
*
* Retorna HTML de la imagen destacada verificando:
* - Si las imágenes destacadas están habilitadas globalmente
* - Si el post tiene una imagen destacada asignada
* - Retorna HTML de la imagen o cadena vacía (sin placeholder)
*
* Tamaños disponibles:
* - roi-featured-large: 1200x600 (para single posts)
* - roi-featured-medium: 800x400 (para archives)
* - roi-thumbnail: 400x300 (para widgets/sidebars)
*
* @param int|null $post_id ID del post (null = post actual)
* @param string $size Tamaño de imagen registrado (default: roi-featured-large)
* @param array $attr Atributos HTML adicionales para la imagen
* @param bool $force_show Forzar mostrar ignorando configuración (default: false)
* @return string HTML de la imagen o cadena vacía
*/
function roi_get_featured_image($post_id = null, $size = 'roi-featured-large', $attr = array(), $force_show = false) {
// Obtener ID del post actual si no se especifica
if (!$post_id) {
$post_id = get_the_ID();
}
// Si no hay ID válido, retornar vacío
if (!$post_id) {
return '';
}
// Verificar si el post tiene imagen destacada
if (!has_post_thumbnail($post_id)) {
return ''; // No placeholder - retornar vacío
}
// Obtener tipo de post
$post_type = get_post_type($post_id);
// Verificar configuración global desde BD (Clean Architecture)
if (!$force_show) {
// Leer configuración desde wp_roi_theme_component_settings
$is_enabled = roi_get_component_setting('featured-image', 'visibility', 'is_enabled', true);
$show_on_pages = roi_get_component_setting('featured-image', 'visibility', 'show_on_pages', 'posts');
if (!$is_enabled) {
return '';
}
// Verificar tipo de contenido según configuración
if ($show_on_pages === 'posts' && $post_type !== 'post') {
return '';
}
if ($show_on_pages === 'pages' && $post_type !== 'page') {
return '';
}
// 'all' = mostrar en todo
}
// Atributos por defecto con Bootstrap img-fluid
$default_attr = array(
'class' => 'img-fluid featured-image',
'loading' => 'lazy',
'alt' => ''
);
// Merge de atributos
$attr = wp_parse_args($attr, $default_attr);
// Si no hay alt text específico, usar el título del post
if (empty($attr['alt'])) {
$attr['alt'] = get_the_title($post_id);
}
// Obtener HTML de la imagen
$thumbnail = get_the_post_thumbnail($post_id, $size, $attr);
// Si no hay thumbnail, retornar vacío
if (empty($thumbnail)) {
return '';
}
// Retornar HTML de la imagen sin contenedor adicional
return $thumbnail;
}
/**
* Muestra la imagen destacada de un post
*
* Template tag para usar directamente en templates.
* Echo wrapper de roi_get_featured_image().
*
* Uso en templates:
* <?php roi_the_featured_image(); ?>
* <?php roi_the_featured_image(null, 'roi-featured-medium'); ?>
*
* @param int|null $post_id ID del post (null = post actual)
* @param string $size Tamaño de imagen registrado
* @param array $attr Atributos HTML adicionales
* @param bool $force_show Forzar mostrar ignorando configuración
*/
function roi_the_featured_image($post_id = null, $size = 'roi-featured-large', $attr = array(), $force_show = false) {
echo roi_get_featured_image($post_id, $size, $attr, $force_show);
}
/**
* Obtiene HTML de thumbnail para archives y loops
*
* Versión optimizada para listados con tamaño medium y link al post.
* Incluye clases responsive de Bootstrap.
*
* @param int|null $post_id ID del post (null = post actual)
* @param bool $with_link Envolver en enlace al post (default: true)
* @return string HTML del thumbnail o cadena vacía
*/
function roi_get_post_thumbnail($post_id = null, $with_link = true) {
// Obtener ID del post actual si no se especifica
if (!$post_id) {
$post_id = get_the_ID();
}
// Si no hay ID válido, retornar vacío
if (!$post_id) {
return '';
}
// Verificar si el post tiene imagen destacada
if (!has_post_thumbnail($post_id)) {
return ''; // No placeholder - retornar vacío
}
// Obtener la imagen con clases Bootstrap
$image = get_the_post_thumbnail($post_id, 'roi-featured-medium', array(
'class' => 'img-fluid post-thumbnail',
'loading' => 'lazy',
'alt' => get_the_title($post_id)
));
// Si no hay imagen, retornar vacío
if (!$image) {
return '';
}
// Construir HTML
$html = '';
if ($with_link) {
$html .= '<a href="' . esc_url(get_permalink($post_id)) . '" class="post-thumbnail-link d-block" aria-label="' . esc_attr(get_the_title($post_id)) . '">';
}
$html .= $image;
if ($with_link) {
$html .= '</a>';
}
return $html;
}
/**
* Muestra el thumbnail del post para archives
*
* Template tag para usar directamente en template-parts.
* Echo wrapper de roi_get_post_thumbnail().
*
* Uso en templates:
* <?php roi_the_post_thumbnail(); ?>
* <?php roi_the_post_thumbnail(null, false); // sin link ?>
*
* @param int|null $post_id ID del post (null = post actual)
* @param bool $with_link Envolver en enlace al post
*/
function roi_the_post_thumbnail($post_id = null, $with_link = true) {
echo roi_get_post_thumbnail($post_id, $with_link);
}
/**
* Obtiene HTML de thumbnail pequeño para widgets/sidebars
*
* Versión mini para listados compactos en sidebars.
* Usa el tamaño roi-thumbnail (400x300).
*
* @param int|null $post_id ID del post (null = post actual)
* @param bool $with_link Envolver en enlace al post (default: true)
* @return string HTML del thumbnail o cadena vacía
*/
function roi_get_post_thumbnail_small($post_id = null, $with_link = true) {
// Obtener ID del post actual si no se especifica
if (!$post_id) {
$post_id = get_the_ID();
}
// Si no hay ID válido, retornar vacío
if (!$post_id) {
return '';
}
// Verificar si el post tiene imagen destacada
if (!has_post_thumbnail($post_id)) {
return ''; // No placeholder - retornar vacío
}
// Obtener la imagen
$image = get_the_post_thumbnail($post_id, 'roi-thumbnail', array(
'class' => 'img-fluid post-thumbnail-small',
'loading' => 'lazy',
'alt' => get_the_title($post_id)
));
// Si no hay imagen, retornar vacío
if (!$image) {
return '';
}
// Construir HTML
$html = '';
if ($with_link) {
$html .= '<a href="' . esc_url(get_permalink($post_id)) . '" class="post-thumbnail-link-small d-block" aria-label="' . esc_attr(get_the_title($post_id)) . '">';
}
$html .= $image;
if ($with_link) {
$html .= '</a>';
}
return $html;
}
/**
* Muestra el thumbnail pequeño del post
*
* Template tag para usar en widgets y sidebars.
* Echo wrapper de roi_get_post_thumbnail_small().
*
* @param int|null $post_id ID del post (null = post actual)
* @param bool $with_link Envolver en enlace al post
*/
function roi_the_post_thumbnail_small($post_id = null, $with_link = true) {
echo roi_get_post_thumbnail_small($post_id, $with_link);
}
/**
* Verifica si se debe mostrar la imagen destacada según configuración
*
* Función helper para usar en condicionales de templates.
* Útil para estructuras if/else en templates.
*
* Uso en templates:
* <?php if (roi_should_show_featured_image()): ?>
* <div class="has-thumbnail">...</div>
* <?php endif; ?>
*
* @param int|null $post_id ID del post (null = post actual)
* @return bool True si debe mostrarse, false en caso contrario
*/
function roi_should_show_featured_image($post_id = null) {
// Obtener ID del post actual si no se especifica
if (!$post_id) {
$post_id = get_the_ID();
}
// Si no hay ID válido, retornar false
if (!$post_id) {
return false;
}
// Verificar si el post tiene imagen destacada
if (!has_post_thumbnail($post_id)) {
return false;
}
// Obtener tipo de post
$post_type = get_post_type($post_id);
// Leer configuración desde BD (Clean Architecture)
$is_enabled = roi_get_component_setting('featured-image', 'visibility', 'is_enabled', true);
$show_on_pages = roi_get_component_setting('featured-image', 'visibility', 'show_on_pages', 'posts');
if (!$is_enabled) {
return false;
}
// Verificar tipo de contenido según configuración
if ($show_on_pages === 'posts' && $post_type !== 'post') {
return false;
}
if ($show_on_pages === 'pages' && $post_type !== 'page') {
return false;
}
return true;
}
/**
* Obtiene la URL de la imagen destacada
*
* Útil para backgrounds CSS o meta tags de redes sociales (Open Graph, Twitter Cards).
*
* Uso:
* $image_url = roi_get_featured_image_url();
* echo '<div style="background-image: url(' . $image_url . ')"></div>';
*
* @param int|null $post_id ID del post (null = post actual)
* @param string $size Tamaño de imagen registrado (default: roi-featured-large)
* @return string URL de la imagen o cadena vacía
*/
function roi_get_featured_image_url($post_id = null, $size = 'roi-featured-large') {
// Obtener ID del post actual si no se especifica
if (!$post_id) {
$post_id = get_the_ID();
}
// Si no hay ID válido, retornar vacío
if (!$post_id) {
return '';
}
// Verificar si el post tiene imagen destacada
if (!has_post_thumbnail($post_id)) {
return ''; // No placeholder - retornar vacío
}
// Obtener URL de la imagen
$image_url = get_the_post_thumbnail_url($post_id, $size);
return $image_url ? esc_url($image_url) : '';
}
/**
* Obtiene el contenedor responsive de imagen destacada con aspect ratio
*
* Incluye aspect ratio CSS y lazy loading para mejor rendimiento.
* Evita layout shift (CLS) con ratio predefinido usando Bootstrap ratio utility.
*
* Uso en templates:
* <?php echo roi_get_featured_image_responsive(); ?>
* <?php echo roi_get_featured_image_responsive(null, 'roi-featured-medium', '16/9'); ?>
*
* @param int|null $post_id ID del post (null = post actual)
* @param string $size Tamaño de imagen registrado (default: roi-featured-large)
* @param string $aspect_ratio Ratio CSS - '2/1' para 2:1, '16/9', etc. (default: '2/1')
* @return string HTML del contenedor con imagen o cadena vacía
*/
function roi_get_featured_image_responsive($post_id = null, $size = 'roi-featured-large', $aspect_ratio = '2/1') {
// Obtener la imagen
$image = roi_get_featured_image($post_id, $size);
// Si no hay imagen, retornar vacío
if (empty($image)) {
return '';
}
// Construir contenedor responsive con Bootstrap ratio y aspect-ratio CSS
$html = '<div class="featured-image-wrapper ratio" style="--bs-aspect-ratio: ' . esc_attr($aspect_ratio) . '; aspect-ratio: ' . esc_attr($aspect_ratio) . ';">';
$html .= $image;
$html .= '</div>';
return $html;
}
/**
* Muestra el contenedor responsive de imagen destacada
*
* Template tag para usar directamente en templates.
* Echo wrapper de roi_get_featured_image_responsive().
*
* @param int|null $post_id ID del post (null = post actual)
* @param string $size Tamaño de imagen registrado
* @param string $aspect_ratio Ratio CSS (ej: '16/9', '2/1')
*/
function roi_the_featured_image_responsive($post_id = null, $size = 'roi-featured-large', $aspect_ratio = '2/1') {
echo roi_get_featured_image_responsive($post_id, $size, $aspect_ratio);
}
/**
* Verifica si las imágenes destacadas están habilitadas para un tipo de post
*
* Lee configuración desde BD (Clean Architecture)
*
* @param string $post_type Tipo de post (vacío = post actual)
* @return bool True si habilitadas, false en caso contrario
*/
function roi_is_featured_image_enabled($post_type = '') {
if (empty($post_type)) {
$post_type = get_post_type();
}
if (!$post_type) {
return true; // Default habilitado
}
// Leer configuración desde BD (Clean Architecture)
$is_enabled = roi_get_component_setting('featured-image', 'visibility', 'is_enabled', true);
$show_on_pages = roi_get_component_setting('featured-image', 'visibility', 'show_on_pages', 'posts');
if (!$is_enabled) {
return false;
}
// Verificar tipo de contenido según configuración
if ($show_on_pages === 'posts' && $post_type !== 'post') {
return false;
}
if ($show_on_pages === 'pages' && $post_type !== 'page') {
return false;
}
return true;
}
// =============================================================================
// NOTA: Customizer eliminado - Clean Architecture
// La configuración de imágenes destacadas se gestiona desde:
// - Admin Panel: Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
// - Base de datos: wp_roi_theme_component_settings (component_name = 'featured-image')
// =============================================================================

500
Inc/image-optimization.php Normal file
View File

@@ -0,0 +1,500 @@
<?php
/**
* Image Optimization Functions
*
* Handles responsive images, WebP/AVIF support, lazy loading, and image preloading.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Enable AVIF image support
*/
function roi_enable_avif_support($mime_types) {
$mime_types['avif'] = 'image/avif';
return $mime_types;
}
add_filter('upload_mimes', 'roi_enable_avif_support');
/**
* Add AVIF to allowed file extensions
*/
function roi_allow_avif_extension($types, $file, $filename, $mimes) {
if (false !== strpos($filename, '.avif')) {
$types['ext'] = 'avif';
$types['type'] = 'image/avif';
}
return $types;
}
add_filter('wp_check_filetype_and_ext', 'roi_allow_avif_extension', 10, 4);
/**
* Configure custom image sizes
* Already defined in functions.php, but we can add more if needed
*/
function roi_setup_additional_image_sizes() {
// Add support for additional modern image sizes
add_image_size('roi-hero', 1920, 800, true); // Hero images
add_image_size('roi-card', 600, 400, true); // Card thumbnails
add_image_size('roi-thumbnail-2x', 800, 600, true); // Retina thumbnails
}
add_action('after_setup_theme', 'roi_setup_additional_image_sizes');
/**
* Add custom image sizes to media library dropdown
*/
function roi_custom_image_sizes($sizes) {
return array_merge($sizes, array(
'roi-thumbnail' => __('Thumbnail (400x300)', 'roi-theme'),
'roi-medium' => __('Medium (800x600)', 'roi-theme'),
'roi-large' => __('Large (1200x900)', 'roi-theme'),
'roi-featured-large' => __('Featured Large (1200x600)', 'roi-theme'),
'roi-featured-medium' => __('Featured Medium (800x400)', 'roi-theme'),
'roi-hero' => __('Hero (1920x800)', 'roi-theme'),
'roi-card' => __('Card (600x400)', 'roi-theme'),
));
}
add_filter('image_size_names_choose', 'roi_custom_image_sizes');
/**
* Generate srcset and sizes attributes for responsive images
*
* @param int $attachment_id Image attachment ID
* @param string $size Image size
* @param array $additional_sizes Additional sizes to include in srcset
* @return array Array with 'srcset' and 'sizes' attributes
*/
function roi_get_responsive_image_attrs($attachment_id, $size = 'full', $additional_sizes = array()) {
if (empty($attachment_id)) {
return array('srcset' => '', 'sizes' => '');
}
// Get default WordPress srcset
$srcset = wp_get_attachment_image_srcset($attachment_id, $size);
// Get default WordPress sizes attribute
$sizes = wp_get_attachment_image_sizes($attachment_id, $size);
// Add additional sizes if specified
if (!empty($additional_sizes)) {
$srcset_array = array();
foreach ($additional_sizes as $additional_size) {
$image = wp_get_attachment_image_src($attachment_id, $additional_size);
if ($image) {
$srcset_array[] = $image[0] . ' ' . $image[1] . 'w';
}
}
if (!empty($srcset_array)) {
$srcset .= ', ' . implode(', ', $srcset_array);
}
}
return array(
'srcset' => $srcset,
'sizes' => $sizes,
);
}
/**
* Output responsive image with lazy loading
*
* @param int $attachment_id Image attachment ID
* @param string $size Image size
* @param array $attr Additional image attributes
* @param bool $lazy Enable lazy loading (default: true)
* @return string Image HTML
*/
function roi_get_responsive_image($attachment_id, $size = 'full', $attr = array(), $lazy = true) {
if (empty($attachment_id)) {
return '';
}
// Get responsive attributes
$responsive_attrs = roi_get_responsive_image_attrs($attachment_id, $size);
// Merge default attributes with custom ones
$default_attr = array(
'loading' => $lazy ? 'lazy' : 'eager',
'decoding' => 'async',
);
// Add srcset and sizes if available
if (!empty($responsive_attrs['srcset'])) {
$default_attr['srcset'] = $responsive_attrs['srcset'];
}
if (!empty($responsive_attrs['sizes'])) {
$default_attr['sizes'] = $responsive_attrs['sizes'];
}
$attr = array_merge($default_attr, $attr);
return wp_get_attachment_image($attachment_id, $size, false, $attr);
}
/**
* Enable lazy loading by default for all images
*/
function roi_add_lazy_loading_to_images($attr, $attachment, $size) {
// Don't add lazy loading if explicitly disabled
if (isset($attr['loading']) && $attr['loading'] === 'eager') {
return $attr;
}
// Add lazy loading by default
if (!isset($attr['loading'])) {
$attr['loading'] = 'lazy';
}
// Add async decoding for better performance
if (!isset($attr['decoding'])) {
$attr['decoding'] = 'async';
}
return $attr;
}
add_filter('wp_get_attachment_image_attributes', 'roi_add_lazy_loading_to_images', 10, 3);
/**
* Add lazy loading to content images
*/
function roi_add_lazy_loading_to_content($content) {
// Don't process if empty
if (empty($content)) {
return $content;
}
// Add loading="lazy" to images that don't have it
$content = preg_replace('/<img(?![^>]*loading=)/', '<img loading="lazy" decoding="async"', $content);
return $content;
}
add_filter('the_content', 'roi_add_lazy_loading_to_content', 20);
/**
* Preload critical images (LCP images)
* This should be called for above-the-fold images
*
* @param int $attachment_id Image attachment ID
* @param string $size Image size
*/
function roi_preload_image($attachment_id, $size = 'full') {
if (empty($attachment_id)) {
return;
}
$image_src = wp_get_attachment_image_src($attachment_id, $size);
if (!$image_src) {
return;
}
$srcset = wp_get_attachment_image_srcset($attachment_id, $size);
$sizes = wp_get_attachment_image_sizes($attachment_id, $size);
// Detect image format
$image_url = $image_src[0];
$image_type = 'image/jpeg'; // default
if (strpos($image_url, '.avif') !== false) {
$image_type = 'image/avif';
} elseif (strpos($image_url, '.webp') !== false) {
$image_type = 'image/webp';
} elseif (strpos($image_url, '.png') !== false) {
$image_type = 'image/png';
}
// Build preload link
$preload = sprintf(
'<link rel="preload" as="image" href="%s" type="%s"',
esc_url($image_url),
esc_attr($image_type)
);
if ($srcset) {
$preload .= sprintf(' imagesrcset="%s"', esc_attr($srcset));
}
if ($sizes) {
$preload .= sprintf(' imagesizes="%s"', esc_attr($sizes));
}
$preload .= '>';
echo $preload . "\n";
}
/**
* Preload featured image for single posts (LCP optimization)
*/
function roi_preload_featured_image() {
// Only on single posts/pages with featured images
if (!is_singular() || !has_post_thumbnail()) {
return;
}
// Get the featured image ID
$thumbnail_id = get_post_thumbnail_id();
// Determine the size based on the post type
$size = 'roi-featured-large';
// Preload the image
roi_preload_image($thumbnail_id, $size);
}
add_action('wp_head', 'roi_preload_featured_image', 5);
/**
* Enable fetchpriority attribute for featured images
*/
function roi_add_fetchpriority_to_featured_image($attr, $attachment, $size) {
// Only add fetchpriority="high" to featured images on singular pages
if (is_singular() && get_post_thumbnail_id() === $attachment->ID) {
$attr['fetchpriority'] = 'high';
$attr['loading'] = 'eager'; // Don't lazy load LCP images
}
return $attr;
}
add_filter('wp_get_attachment_image_attributes', 'roi_add_fetchpriority_to_featured_image', 20, 3);
/**
* Optimize image quality for uploads
*/
function roi_optimize_image_quality($quality, $mime_type) {
// Set quality to 85% for better file size without visible quality loss
if ($mime_type === 'image/jpeg') {
return 85;
}
return $quality;
}
add_filter('jpeg_quality', 'roi_optimize_image_quality', 10, 2);
add_filter('wp_editor_set_quality', 'roi_optimize_image_quality', 10, 2);
/**
* Add picture element support for WebP/AVIF with fallbacks
*
* @param int $attachment_id Image attachment ID
* @param string $size Image size
* @param array $attr Additional attributes
* @return string Picture element HTML
*/
function roi_get_picture_element($attachment_id, $size = 'full', $attr = array()) {
if (empty($attachment_id)) {
return '';
}
$image_src = wp_get_attachment_image_src($attachment_id, $size);
if (!$image_src) {
return '';
}
$srcset = wp_get_attachment_image_srcset($attachment_id, $size);
$sizes = wp_get_attachment_image_sizes($attachment_id, $size);
$alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
// Default attributes
$default_attr = array(
'loading' => 'lazy',
'decoding' => 'async',
'alt' => $alt,
);
$attr = array_merge($default_attr, $attr);
// Build picture element
$picture = '<picture>';
// Add AVIF source if available
$avif_url = str_replace(array('.jpg', '.jpeg', '.png', '.webp'), '.avif', $image_src[0]);
if ($avif_url !== $image_src[0]) {
$picture .= sprintf(
'<source type="image/avif" srcset="%s" sizes="%s">',
esc_attr($avif_url),
esc_attr($sizes)
);
}
// Add WebP source if available
$webp_url = str_replace(array('.jpg', '.jpeg', '.png'), '.webp', $image_src[0]);
if ($webp_url !== $image_src[0]) {
$picture .= sprintf(
'<source type="image/webp" srcset="%s" sizes="%s">',
esc_attr($webp_url),
esc_attr($sizes)
);
}
// Fallback img tag
$picture .= wp_get_attachment_image($attachment_id, $size, false, $attr);
$picture .= '</picture>';
return $picture;
}
/**
* Configurar threshold de escala de imágenes grandes
* WordPress 5.3+ escala imágenes mayores a 2560px por defecto
* Mantenemos 2560px como límite para balance entre calidad y rendimiento
*/
function roi_big_image_size_threshold($threshold) {
// Mantener 2560px como threshold (no desactivar completamente)
return 2560;
}
add_filter('big_image_size_threshold', 'roi_big_image_size_threshold');
/**
* Set maximum srcset image width
*/
function roi_max_srcset_image_width($max_width, $size_array) {
// Limit srcset to images up to 2560px wide
return 2560;
}
add_filter('max_srcset_image_width', 'roi_max_srcset_image_width', 10, 2);
/**
* Habilitar generación automática de WebP en WordPress
* WordPress 5.8+ soporta WebP nativamente si el servidor tiene soporte
*/
function roi_enable_webp_generation($editors) {
// Verificar que GD o Imagick tengan soporte WebP
if (extension_loaded('gd')) {
$gd_info = gd_info();
if (!empty($gd_info['WebP Support'])) {
// WebP está soportado en GD
return $editors;
}
}
if (extension_loaded('imagick')) {
$imagick = new Imagick();
$formats = $imagick->queryFormats('WEBP');
if (count($formats) > 0) {
// WebP está soportado en Imagick
return $editors;
}
}
return $editors;
}
add_filter('wp_image_editors', 'roi_enable_webp_generation');
/**
* Configurar tipos MIME adicionales para WebP y AVIF
*/
function roi_additional_mime_types($mimes) {
// WebP ya está soportado en WordPress 5.8+, pero lo agregamos por compatibilidad
if (!isset($mimes['webp'])) {
$mimes['webp'] = 'image/webp';
}
// AVIF soportado desde WordPress 6.5+
if (!isset($mimes['avif'])) {
$mimes['avif'] = 'image/avif';
}
return $mimes;
}
add_filter('mime_types', 'roi_additional_mime_types');
/**
* Remover tamaños de imagen no utilizados para ahorrar espacio
*/
function roi_remove_unused_image_sizes($sizes) {
// Remover tamaños de WordPress que no usamos
unset($sizes['medium_large']); // 768px - no necesario con nuestros tamaños custom
unset($sizes['1536x1536']); // 2x medium_large - no necesario
unset($sizes['2048x2048']); // 2x large - no necesario
return $sizes;
}
add_filter('intermediate_image_sizes_advanced', 'roi_remove_unused_image_sizes');
/**
* Agregar sizes attribute personalizado según contexto
* Mejora la selección del tamaño correcto de imagen por el navegador
*/
function roi_responsive_image_sizes_attr($sizes, $size, $image_src, $image_meta, $attachment_id) {
// Para imágenes destacadas grandes (single posts)
if ($size === 'roi-featured-large' || $size === 'roi-large') {
$sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px';
}
// Para imágenes destacadas medianas (archives)
elseif ($size === 'roi-featured-medium' || $size === 'roi-medium') {
$sizes = '(max-width: 768px) 100vw, (max-width: 992px) 50vw, 800px';
}
// Para thumbnails (widgets, related posts)
elseif ($size === 'roi-thumbnail') {
$sizes = '(max-width: 576px) 100vw, 400px';
}
// Para hero images
elseif ($size === 'roi-hero') {
$sizes = '100vw';
}
return $sizes;
}
add_filter('wp_calculate_image_sizes', 'roi_responsive_image_sizes_attr', 10, 5);
/**
* Forzar regeneración de metadatos de imagen para incluir WebP/AVIF
* Solo se ejecuta cuando sea necesario
*/
function roi_maybe_regenerate_image_metadata($metadata, $attachment_id) {
// Verificar si ya tiene formatos modernos generados
if (!empty($metadata) && is_array($metadata)) {
// WordPress maneja automáticamente la generación de sub-formatos
return $metadata;
}
return $metadata;
}
add_filter('wp_generate_attachment_metadata', 'roi_maybe_regenerate_image_metadata', 10, 2);
/**
* Excluir lazy loading en la primera imagen del contenido (posible LCP)
*/
function roi_skip_lazy_loading_first_image($content) {
// Solo en posts/páginas singulares
if (!is_singular()) {
return $content;
}
// Contar si es la primera imagen
static $first_image = true;
if ($first_image) {
// Cambiar loading="lazy" a loading="eager" en la primera imagen
$content = preg_replace(
'/<img([^>]+?)loading=["\']lazy["\']/',
'<img$1loading="eager"',
$content,
1 // Solo la primera coincidencia
);
$first_image = false;
}
return $content;
}
add_filter('the_content', 'roi_skip_lazy_loading_first_image', 15);
/**
* Agregar soporte para formatos de imagen modernos en subsizes
* WordPress 5.8+ genera automáticamente WebP si está disponible
*/
function roi_enable_image_subsizes($metadata, $attachment_id, $context) {
if ($context !== 'create') {
return $metadata;
}
// WordPress genera automáticamente WebP subsizes si está disponible
// Este filtro asegura que se ejecute correctamente
return $metadata;
}
add_filter('wp_generate_attachment_metadata', 'roi_enable_image_subsizes', 20, 3);

200
Inc/nav-walker.php Normal file
View File

@@ -0,0 +1,200 @@
<?php
/**
* Bootstrap 5 Nav Walker
*
* Custom Walker para wp_nav_menu() compatible con Bootstrap 5.
* Genera markup correcto para navbar, dropdowns y menús responsive.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Class WP_Bootstrap_Navwalker
*
* Bootstrap 5 compatible navigation menu walker
*/
class WP_Bootstrap_Navwalker extends Walker_Nav_Menu {
/**
* Starts the list before the elements are added.
*
* @param string $output Used to append additional content (passed by reference).
* @param int $depth Depth of menu item. Used for padding.
* @param stdClass $args An object of wp_nav_menu() arguments.
*/
public function start_lvl(&$output, $depth = 0, $args = null) {
if (isset($args->item_spacing) && 'discard' === $args->item_spacing) {
$t = '';
$n = '';
} else {
$t = "\t";
$n = "\n";
}
$indent = str_repeat($t, $depth);
// Dropdown menu classes
$classes = array('dropdown-menu');
$class_names = join(' ', apply_filters('nav_menu_submenu_css_class', $classes, $args, $depth));
$class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : '';
$output .= "{$n}{$indent}<ul$class_names>{$n}";
}
/**
* Starts the element output.
*
* @param string $output Used to append additional content (passed by reference).
* @param WP_Post $item Menu item data object.
* @param int $depth Depth of menu item. Used for padding.
* @param stdClass $args An object of wp_nav_menu() arguments.
* @param int $id Current item ID.
*/
public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0) {
if (isset($args->item_spacing) && 'discard' === $args->item_spacing) {
$t = '';
$n = '';
} else {
$t = "\t";
$n = "\n";
}
$indent = ($depth) ? str_repeat($t, $depth) : '';
$classes = empty($item->classes) ? array() : (array) $item->classes;
$classes[] = 'menu-item-' . $item->ID;
// Add Bootstrap classes based on depth
if ($depth === 0) {
$classes[] = 'nav-item';
}
// Check if menu item has children
$has_children = in_array('menu-item-has-children', $classes);
if ($has_children && $depth === 0) {
$classes[] = 'dropdown';
}
$class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth));
$class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : '';
$id = apply_filters('nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth);
$id = $id ? ' id="' . esc_attr($id) . '"' : '';
// Output <li>
if ($depth === 0) {
$output .= $indent . '<li' . $id . $class_names . '>';
} else {
$output .= $indent . '<li' . $class_names . '>';
}
// Link attributes
$atts = array();
$atts['title'] = !empty($item->attr_title) ? $item->attr_title : '';
$atts['target'] = !empty($item->target) ? $item->target : '';
$atts['rel'] = !empty($item->xfn) ? $item->xfn : '';
$atts['href'] = !empty($item->url) ? $item->url : '';
// Add Bootstrap nav-link class for depth 0
if ($depth === 0) {
$atts['class'] = 'nav-link';
} else {
$atts['class'] = 'dropdown-item';
}
// Add dropdown-toggle class and attributes for parent items
if ($has_children && $depth === 0) {
$atts['class'] .= ' dropdown-toggle';
$atts['data-bs-toggle'] = 'dropdown';
$atts['aria-expanded'] = 'false';
$atts['role'] = 'button';
}
// Add active class for current menu item
if (in_array('current-menu-item', $classes) || in_array('current-menu-parent', $classes)) {
$atts['class'] .= ' active';
$atts['aria-current'] = 'page';
}
$atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth);
$attributes = '';
foreach ($atts as $attr => $value) {
if (!empty($value)) {
$value = ('href' === $attr) ? esc_url($value) : esc_attr($value);
$attributes .= ' ' . $attr . '="' . $value . '"';
}
}
$title = apply_filters('the_title', $item->title, $item->ID);
$title = apply_filters('nav_menu_item_title', $title, $item, $args, $depth);
// Build the link
$item_output = $args->before;
$item_output .= '<a' . $attributes . '>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args);
}
/**
* Traverse elements to create list from elements.
*
* Display one element if the element doesn't have any children otherwise,
* display the element and its children. Will only traverse up to the max
* depth and no ignore elements under that depth. It is possible to set the
* max depth to include all depths, see walk() method.
*
* This method should not be called directly, use the walk() method instead.
*
* @param object $element Data object.
* @param array $children_elements List of elements to continue traversing (passed by reference).
* @param int $max_depth Max depth to traverse.
* @param int $depth Depth of current element.
* @param array $args An array of arguments.
* @param string $output Used to append additional content (passed by reference).
*/
public function display_element($element, &$children_elements, $max_depth, $depth, $args, &$output) {
if (!$element) {
return;
}
$id_field = $this->db_fields['id'];
// Display this element
if (is_object($args[0])) {
$args[0]->has_children = !empty($children_elements[$element->$id_field]);
}
parent::display_element($element, $children_elements, $max_depth, $depth, $args, $output);
}
/**
* Menu Fallback
*
* If this function is assigned to the wp_nav_menu's fallback_cb option
* and a menu has not been assigned to the theme location in the WordPress
* menu manager the function will display a basic menu of all published pages.
*
* @param array $args passed from the wp_nav_menu function.
*/
public static function fallback($args) {
if (current_user_can('edit_theme_options')) {
echo '<ul class="' . esc_attr($args['menu_class']) . '">';
echo '<li class="nav-item">';
echo '<a class="nav-link" href="' . esc_url(admin_url('nav-menus.php')) . '">';
esc_html_e('Crear un menú', 'roi-theme');
echo '</a>';
echo '</li>';
echo '</ul>';
}
}
}

588
Inc/performance.php Normal file
View File

@@ -0,0 +1,588 @@
<?php
/**
* Performance Optimization Functions
*
* Functions to remove WordPress bloat and improve performance.
*
* NOTA: Versión reducida con solo optimizaciones seguras después de
* resolver problemas de memory exhaustion en Issue #22.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Disable WordPress Emojis
*
* Removes emoji detection scripts and styles from WordPress.
* These scripts are loaded on every page but rarely used.
*
* @since 1.0.0
*/
function roi_disable_emojis() {
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
remove_filter( 'the_content_feed', 'wp_staticize_emoji' );
remove_filter( 'comment_text_rss', 'wp_staticize_emoji' );
remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
// Remove from TinyMCE.
add_filter( 'tiny_mce_plugins', 'roi_disable_emojis_tinymce' );
}
add_action( 'init', 'roi_disable_emojis' );
/**
* Filter function used to remove emoji plugin from TinyMCE
*
* @since 1.0.0
* @param array $plugins An array of default TinyMCE plugins.
* @return array Modified array of TinyMCE plugins without emoji plugin.
*/
function roi_disable_emojis_tinymce( $plugins ) {
if ( is_array( $plugins ) ) {
return array_diff( $plugins, array( 'wpemoji' ) );
}
return array();
}
/**
* Disable WordPress oEmbed
*
* Removes oEmbed discovery links and scripts.
* Only disable if you don't need to embed content from other sites.
*
* @since 1.0.0
*/
function roi_disable_oembed() {
// Remove oEmbed discovery links.
remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
// Remove oEmbed-specific JavaScript from the front-end and back-end.
remove_action( 'wp_head', 'wp_oembed_add_host_js' );
// Remove all embeds rewrite rules.
add_filter( 'rewrite_rules_array', 'roi_disable_oembed_rewrites' );
// Remove filter of the oEmbed result before any HTTP requests are made.
remove_filter( 'pre_oembed_result', 'wp_filter_pre_oembed_result', 10 );
}
add_action( 'init', 'roi_disable_oembed', 9999 );
/**
* Remove all rewrite rules related to embeds
*
* @since 1.0.0
* @param array $rules WordPress rewrite rules.
* @return array Modified rewrite rules.
*/
function roi_disable_oembed_rewrites( $rules ) {
foreach ( $rules as $rule => $rewrite ) {
if ( false !== strpos( $rewrite, 'embed=true' ) ) {
unset( $rules[ $rule ] );
}
}
return $rules;
}
/**
* Disable wp-embed.js script
*
* Dequeues the wp-embed.js script that WordPress loads by default.
* This script is used for embedding WordPress posts on other sites.
*
* @since 1.0.0
*/
function roi_dequeue_embed_script() {
wp_deregister_script( 'wp-embed' );
}
add_action( 'wp_footer', 'roi_dequeue_embed_script' );
/**
* Disable WordPress Feeds
*
* Removes RSS, RDF, and Atom feeds.
* Only disable if you don't need feed functionality.
*
* @since 1.0.0
*/
function roi_disable_feeds() {
wp_die(
esc_html__( 'No feed available, please visit our homepage!', 'roi' ),
esc_html__( 'Feed Not Available', 'roi' ),
array(
'response' => 404,
'back_link' => true,
)
);
}
/**
* Remove feed links and redirect feed URLs
*
* @since 1.0.0
*/
function roi_disable_feed_links() {
// Remove feed links from header.
remove_action( 'wp_head', 'feed_links', 2 );
remove_action( 'wp_head', 'feed_links_extra', 3 );
// Redirect feed URLs to homepage.
add_action( 'do_feed', 'roi_disable_feeds', 1 );
add_action( 'do_feed_rdf', 'roi_disable_feeds', 1 );
add_action( 'do_feed_rss', 'roi_disable_feeds', 1 );
add_action( 'do_feed_rss2', 'roi_disable_feeds', 1 );
add_action( 'do_feed_atom', 'roi_disable_feeds', 1 );
add_action( 'do_feed_rss2_comments', 'roi_disable_feeds', 1 );
add_action( 'do_feed_atom_comments', 'roi_disable_feeds', 1 );
}
add_action( 'init', 'roi_disable_feed_links' );
/**
* Disable RSD and Windows Live Writer Manifest
*
* Really Simple Discovery (RSD) and Windows Live Writer (WLW) manifest
* are rarely used and can be safely removed.
*
* @since 1.0.0
*/
function roi_disable_rsd_wlw() {
// Remove RSD link.
remove_action( 'wp_head', 'rsd_link' );
// Remove Windows Live Writer manifest link.
remove_action( 'wp_head', 'wlwmanifest_link' );
}
add_action( 'init', 'roi_disable_rsd_wlw' );
/**
* Disable Dashicons for non-logged users
*
* Dashicons are only needed in the admin area and for logged-in users.
* This removes them from the front-end for visitors.
*
* @since 1.0.0
*/
function roi_disable_dashicons() {
if ( ! is_user_logged_in() ) {
wp_deregister_style( 'dashicons' );
}
}
add_action( 'wp_enqueue_scripts', 'roi_disable_dashicons' );
/**
* Disable Block Library CSS
*
* Removes the default WordPress block library styles.
* Only disable if you're not using the block editor or if you're
* providing your own block styles.
*
* @since 1.0.0
*/
function roi_disable_block_library_css() {
// Remove default block library styles.
wp_dequeue_style( 'wp-block-library' );
wp_dequeue_style( 'wp-block-library-theme' );
// Remove inline global styles.
wp_dequeue_style( 'global-styles' );
// Remove classic theme styles (if not using classic editor).
wp_dequeue_style( 'classic-theme-styles' );
}
add_action( 'wp_enqueue_scripts', 'roi_disable_block_library_css', 100 );
/**
* Remove WordPress version from head and feeds
*
* Removes the WordPress version number for security reasons.
*
* @since 1.0.0
*/
function roi_remove_wp_version() {
return '';
}
add_filter( 'the_generator', 'roi_remove_wp_version' );
/**
* Disable XML-RPC
*
* XML-RPC is often targeted by brute force attacks.
* Disable if you don't need remote publishing functionality.
*
* @since 1.0.0
* @return bool
*/
function roi_disable_xmlrpc() {
return false;
}
add_filter( 'xmlrpc_enabled', 'roi_disable_xmlrpc' );
/**
* Remove jQuery Migrate
*
* jQuery Migrate is used for backwards compatibility but adds extra overhead.
* Only remove if you've verified all scripts work without it.
*
* @since 1.0.0
* @param WP_Scripts $scripts WP_Scripts object.
*/
function roi_remove_jquery_migrate( $scripts ) {
if ( ! is_admin() && isset( $scripts->registered['jquery'] ) ) {
$script = $scripts->registered['jquery'];
if ( $script->deps ) {
// Remove jquery-migrate from dependencies.
$script->deps = array_diff( $script->deps, array( 'jquery-migrate' ) );
}
}
}
add_action( 'wp_default_scripts', 'roi_remove_jquery_migrate' );
/**
* Optimize WordPress Database Queries
*
* Removes unnecessary meta queries for better performance.
*
* @since 1.0.0
*/
function roi_optimize_queries() {
// Remove unnecessary post meta from queries
remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10 );
remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 );
}
add_action( 'init', 'roi_optimize_queries' );
/**
* Disable WordPress Admin Bar for Non-Admins
*
* Reduces HTTP requests for non-admin users.
*
* @since 1.0.0
*/
function roi_disable_admin_bar() {
if ( ! current_user_can( 'administrator' ) && ! is_admin() ) {
show_admin_bar( false );
}
}
add_action( 'after_setup_theme', 'roi_disable_admin_bar' );
/**
* ============================================================================
* RESOURCE HINTS: DNS PREFETCH, PRECONNECT, PRELOAD
* ============================================================================
*/
/**
* Agregar DNS Prefetch y Preconnect para recursos externos
*
* DNS Prefetch: Resuelve DNS antes de que se necesite el recurso
* Preconnect: Establece conexión completa (DNS + TCP + TLS) por anticipado
*
* @since 1.0.0
* @param array $urls Array of resource URLs.
* @param string $relation_type The relation type (dns-prefetch, preconnect, etc.).
* @return array Modified array of resource URLs.
*/
function roi_add_resource_hints( $urls, $relation_type ) {
// DNS Prefetch para recursos externos que no son críticos
if ( 'dns-prefetch' === $relation_type ) {
// CDN de Bootstrap Icons (ya usado en enqueue-scripts.php)
$urls[] = 'https://cdn.jsdelivr.net';
// Google Analytics (si se usa)
$urls[] = 'https://www.google-analytics.com';
$urls[] = 'https://www.googletagmanager.com';
// Google AdSense (si se usa)
$urls[] = 'https://pagead2.googlesyndication.com';
$urls[] = 'https://adservice.google.com';
$urls[] = 'https://googleads.g.doubleclick.net';
}
// Preconnect para recursos críticos externos
if ( 'preconnect' === $relation_type ) {
// CDN de Bootstrap Icons - recurso crítico usado en el header
$urls[] = array(
'href' => 'https://cdn.jsdelivr.net',
'crossorigin' => 'anonymous',
);
}
return $urls;
}
add_filter( 'wp_resource_hints', 'roi_add_resource_hints', 10, 2 );
/**
* Preload de recursos críticos para mejorar LCP
*
* Preload indica al navegador que descargue recursos críticos lo antes posible.
* Útil para fuentes, CSS crítico, y imágenes hero.
*
* @since 1.0.0
*/
function roi_preload_critical_resources() {
// NOTA: Fuentes Poppins se cargan desde Google Fonts (enqueue-scripts.php)
// No se necesita preload de fuentes locales
// Preload del CSS de Bootstrap (crítico para el layout)
$bootstrap_css = get_template_directory_uri() . '/Assets/vendor/bootstrap/css/bootstrap.min.css';
printf(
'<link rel="preload" href="%s" as="style">' . "\n",
esc_url( $bootstrap_css )
);
// Preload del CSS de fuentes (crítico para evitar FOIT/FOUT)
$fonts_css = get_template_directory_uri() . '/Assets/css/css-global-fonts.css';
printf(
'<link rel="preload" href="%s" as="style">' . "\n",
esc_url( $fonts_css )
);
}
add_action( 'wp_head', 'roi_preload_critical_resources', 2 );
/**
* ============================================================================
* OPTIMIZACIÓN DE SCRIPTS Y ESTILOS
* ============================================================================
*/
/**
* Agregar atributos async/defer a scripts específicos
*
* Los scripts con defer se descargan en paralelo pero se ejecutan en orden
* después de que el DOM esté listo.
*
* @since 1.0.0
* @param string $tag The script tag.
* @param string $handle The script handle.
* @return string Modified script tag.
*/
function roi_add_script_attributes( $tag, $handle ) {
// Scripts que deben tener async (no dependen de otros ni del DOM)
$async_scripts = array(
// Google Analytics u otros scripts de tracking
'google-analytics',
'gtag',
);
// Scripts que ya tienen defer via strategy en wp_enqueue_script
// No necesitamos modificarlos aquí ya que WordPress 6.3+ lo maneja
if ( in_array( $handle, $async_scripts, true ) ) {
// Agregar async solo si no tiene defer
if ( false === strpos( $tag, 'defer' ) ) {
$tag = str_replace( ' src', ' async src', $tag );
}
}
return $tag;
}
add_filter( 'script_loader_tag', 'roi_add_script_attributes', 10, 2 );
/**
* Optimizar el Heartbeat API de WordPress
*
* El Heartbeat API hace llamadas AJAX periódicas que pueden afectar el rendimiento.
* Lo desactivamos en el frontend y lo ralentizamos en el admin.
*
* @since 1.0.0
*/
function roi_optimize_heartbeat() {
// Desactivar completamente en el frontend
if ( ! is_admin() ) {
wp_deregister_script( 'heartbeat' );
}
}
add_action( 'init', 'roi_optimize_heartbeat', 1 );
/**
* Modificar configuración del Heartbeat en admin
*
* @since 1.0.0
* @param array $settings Heartbeat settings.
* @return array Modified settings.
*/
function roi_modify_heartbeat_settings( $settings ) {
// Cambiar intervalo de 15 segundos (default) a 60 segundos
$settings['interval'] = 60;
return $settings;
}
add_filter( 'heartbeat_settings', 'roi_modify_heartbeat_settings' );
/**
* ============================================================================
* OPTIMIZACIÓN DE BASE DE DATOS Y QUERIES
* ============================================================================
*/
/**
* Limitar revisiones de posts para reducir tamaño de BD
*
* Esto se debe configurar en wp-config.php, pero lo documentamos aquí:
* define('WP_POST_REVISIONS', 5);
*
* @since 1.0.0
*/
/**
* Optimizar WP_Query para posts relacionados y listados
*
* @since 1.0.0
* @param WP_Query $query The WP_Query instance.
*/
function roi_optimize_main_query( $query ) {
// Solo en queries principales en el frontend
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
// En archivos, limitar posts por página para mejorar rendimiento
if ( $query->is_archive() || $query->is_home() ) {
// No cargar meta innecesaria
$query->set( 'update_post_meta_cache', true );
$query->set( 'update_post_term_cache', true );
// Limitar posts por página si no está configurado
if ( ! $query->get( 'posts_per_page' ) ) {
$query->set( 'posts_per_page', 12 );
}
}
}
add_action( 'pre_get_posts', 'roi_optimize_main_query' );
/**
* Deshabilitar self-pingbacks
*
* Los self-pingbacks ocurren cuando un post enlaza a otro post del mismo sitio.
* Son innecesarios y generan queries adicionales.
*
* @since 1.0.0
* @param array $links An array of post links to ping.
* @return array Modified array without self-pings.
*/
function roi_disable_self_pingbacks( &$links ) {
$home = get_option( 'home' );
foreach ( $links as $l => $link ) {
if ( 0 === strpos( $link, $home ) ) {
unset( $links[ $l ] );
}
}
}
add_action( 'pre_ping', 'roi_disable_self_pingbacks' );
/**
* ============================================================================
* OPTIMIZACIÓN DE RENDER Y LAYOUT
* ============================================================================
*/
/**
* Agregar display=swap a Google Fonts para evitar FOIT
*
* Ya no usamos Google Fonts (fuentes locales), pero dejamos la función
* por si se necesita en el futuro.
*
* @since 1.0.0
* @param string $src The source URL.
* @return string Modified source URL.
*/
function roi_add_font_display_swap( $src ) {
if ( strpos( $src, 'fonts.googleapis.com' ) !== false ) {
$src = add_query_arg( 'display', 'swap', $src );
}
return $src;
}
add_filter( 'style_loader_src', 'roi_add_font_display_swap' );
/**
* Agregar width y height a imágenes para prevenir CLS
*
* WordPress 5.5+ agrega automáticamente width/height, pero aseguramos que esté activo.
*
* @since 1.0.0
* @return bool
*/
function roi_enable_image_dimensions() {
return true;
}
add_filter( 'wp_lazy_loading_enabled', 'roi_enable_image_dimensions' );
/**
* Optimizar buffer de salida HTML
*
* Habilita compresión GZIP si está disponible y no está ya habilitada.
*
* @since 1.0.0
*/
function roi_enable_gzip_compression() {
// Solo en frontend
if ( is_admin() ) {
return;
}
// Verificar si GZIP ya está habilitado
if ( ! ini_get( 'zlib.output_compression' ) && 'ob_gzhandler' !== ini_get( 'output_handler' ) ) {
// Verificar si la extensión está disponible
if ( function_exists( 'gzencode' ) && extension_loaded( 'zlib' ) ) {
// Verificar headers
if ( ! headers_sent() ) {
// Habilitar compresión
ini_set( 'zlib.output_compression', 'On' );
ini_set( 'zlib.output_compression_level', '6' ); // Balance entre compresión y CPU
}
}
}
}
add_action( 'template_redirect', 'roi_enable_gzip_compression', 0 );
/**
* ============================================================================
* FUNCIONES AUXILIARES
* ============================================================================
*/
/**
* Limpiar caché de transients expirados periódicamente
*
* Los transients expirados se acumulan en la base de datos.
*
* @since 1.0.0
*/
function roi_cleanup_expired_transients() {
global $wpdb;
// Eliminar transients expirados (solo los del tema)
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s AND option_value < %d",
$wpdb->esc_like( '_transient_timeout_roi_' ) . '%',
time()
)
);
}
// Ejecutar limpieza semanalmente
add_action( 'roi_weekly_cleanup', 'roi_cleanup_expired_transients' );
// Registrar evento cron si no existe
if ( ! wp_next_scheduled( 'roi_weekly_cleanup' ) ) {
wp_schedule_event( time(), 'weekly', 'roi_weekly_cleanup' );
}
/*
* NOTA: Funciones previamente deshabilitadas han sido reimplementadas
* con mejoras para evitar loops infinitos y problemas de memoria.
*
* - Resource hints (dns-prefetch, preconnect) - REACTIVADO
* - Preload de recursos críticos - REACTIVADO
* - Optimización del Heartbeat API - REACTIVADO
* - Remoción de query strings - REACTIVADO (solo para assets propios)
* - Script attributes (defer/async) - REACTIVADO
*/

294
Inc/related-posts.php Normal file
View File

@@ -0,0 +1,294 @@
<?php
/**
* Related Posts Functionality
*
* Provides configurable related posts functionality with Bootstrap grid support.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Get related posts based on categories
*
* @param int $post_id The post ID to get related posts for
* @return WP_Query|false Query object with related posts or false if none found
*/
function roi_get_related_posts($post_id) {
// Get post categories
$categories = wp_get_post_categories($post_id);
if (empty($categories)) {
return false;
}
// Get number of posts to display (default: 3)
$posts_per_page = get_option('roi_related_posts_count', 3);
// Query arguments
$args = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $posts_per_page,
'post__not_in' => array($post_id),
'category__in' => $categories,
'orderby' => 'rand',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
);
// Allow filtering of query args
$args = apply_filters('roi_related_posts_args', $args, $post_id);
// Get related posts
$related_query = new WP_Query($args);
return $related_query->have_posts() ? $related_query : false;
}
/**
* Display related posts section
*
* @param int|null $post_id Optional. Post ID. Default is current post.
* @return void
*/
function roi_display_related_posts($post_id = null) {
// Get post ID
if (!$post_id) {
$post_id = get_the_ID();
}
// Check if related posts are enabled
$enabled = get_option('roi_related_posts_enabled', true);
if (!$enabled) {
return;
}
// Get related posts
$related_query = roi_get_related_posts($post_id);
if (!$related_query) {
return;
}
// Get configuration options
$title = get_option('roi_related_posts_title', __('Related Posts', 'roi-theme'));
$columns = get_option('roi_related_posts_columns', 3);
$show_excerpt = get_option('roi_related_posts_show_excerpt', true);
$show_date = get_option('roi_related_posts_show_date', true);
$show_category = get_option('roi_related_posts_show_category', true);
$excerpt_length = get_option('roi_related_posts_excerpt_length', 20);
$background_colors = get_option('roi_related_posts_bg_colors', array(
'#1a73e8', // Blue
'#e91e63', // Pink
'#4caf50', // Green
'#ff9800', // Orange
'#9c27b0', // Purple
'#00bcd4', // Cyan
));
// Calculate Bootstrap column class
$col_class = roi_get_column_class($columns);
// Start output
?>
<section class="related-posts-section">
<div class="related-posts-container">
<?php if ($title) : ?>
<h2 class="related-posts-title"><?php echo esc_html($title); ?></h2>
<?php endif; ?>
<div class="row g-4">
<?php
$color_index = 0;
while ($related_query->have_posts()) :
$related_query->the_post();
$has_thumbnail = has_post_thumbnail();
// Get background color for posts without image
$bg_color = $background_colors[$color_index % count($background_colors)];
$color_index++;
?>
<div class="<?php echo esc_attr($col_class); ?>">
<article class="related-post-card <?php echo $has_thumbnail ? 'has-thumbnail' : 'no-thumbnail'; ?>">
<a href="<?php the_permalink(); ?>" class="related-post-link">
<?php if ($has_thumbnail) : ?>
<!-- Card with Image -->
<div class="related-post-thumbnail">
<?php
the_post_thumbnail('roi-thumbnail', array(
'alt' => the_title_attribute(array('echo' => false)),
'loading' => 'lazy',
));
?>
<?php if ($show_category) : ?>
<?php
$categories = get_the_category();
if (!empty($categories)) :
$category = $categories[0];
?>
<span class="related-post-category">
<?php echo esc_html($category->name); ?>
</span>
<?php endif; ?>
<?php endif; ?>
</div>
<?php else : ?>
<!-- Card without Image - Color Background -->
<div class="related-post-no-image" style="background-color: <?php echo esc_attr($bg_color); ?>;">
<div class="related-post-no-image-content">
<h3 class="related-post-no-image-title">
<?php the_title(); ?>
</h3>
<?php if ($show_category) : ?>
<?php
$categories = get_the_category();
if (!empty($categories)) :
$category = $categories[0];
?>
<span class="related-post-category no-image">
<?php echo esc_html($category->name); ?>
</span>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<div class="related-post-content">
<?php if ($has_thumbnail) : ?>
<h3 class="related-post-title">
<?php the_title(); ?>
</h3>
<?php endif; ?>
<?php if ($show_excerpt && $excerpt_length > 0) : ?>
<div class="related-post-excerpt">
<?php echo wp_trim_words(get_the_excerpt(), $excerpt_length, '...'); ?>
</div>
<?php endif; ?>
<?php if ($show_date) : ?>
<div class="related-post-meta">
<time class="related-post-date" datetime="<?php echo esc_attr(get_the_date('c')); ?>">
<?php echo esc_html(get_the_date()); ?>
</time>
</div>
<?php endif; ?>
</div>
</a>
</article>
</div>
<?php endwhile; ?>
</div><!-- .row -->
</div><!-- .related-posts-container -->
</section><!-- .related-posts-section -->
<?php
// Reset post data
wp_reset_postdata();
}
/**
* Get Bootstrap column class based on number of columns
*
* @param int $columns Number of columns (1-4)
* @return string Bootstrap column classes
*/
function roi_get_column_class($columns) {
$columns = absint($columns);
switch ($columns) {
case 1:
return 'col-12';
case 2:
return 'col-12 col-md-6';
case 3:
return 'col-12 col-sm-6 col-lg-4';
case 4:
return 'col-12 col-sm-6 col-lg-3';
default:
return 'col-12 col-sm-6 col-lg-4'; // Default to 3 columns
}
}
/**
* Hook related posts display after post content
*/
function roi_hook_related_posts() {
if (is_single() && !is_attachment()) {
roi_display_related_posts();
}
}
add_action('roi_after_post_content', 'roi_hook_related_posts');
/**
* Enqueue related posts styles
*/
function roi_enqueue_related_posts_styles() {
if (is_single() && !is_attachment()) {
$enabled = get_option('roi_related_posts_enabled', true);
if ($enabled) {
wp_enqueue_style(
'roirelated-posts',
get_template_directory_uri() . '/Assets/css/related-posts.css',
array('roibootstrap'),
ROI_VERSION,
'all'
);
}
}
}
add_action('wp_enqueue_scripts', 'roi_enqueue_related_posts_styles');
/**
* Register related posts settings
* These can be configured via theme options or customizer
*/
function roi_related_posts_default_options() {
// Set default options if they don't exist
$defaults = array(
'roi_related_posts_enabled' => true,
'roi_related_posts_title' => __('Related Posts', 'roi-theme'),
'roi_related_posts_count' => 3,
'roi_related_posts_columns' => 3,
'roi_related_posts_show_excerpt' => true,
'roi_related_posts_excerpt_length' => 20,
'roi_related_posts_show_date' => true,
'roi_related_posts_show_category' => true,
'roi_related_posts_bg_colors' => array(
'#1a73e8', // Blue
'#e91e63', // Pink
'#4caf50', // Green
'#ff9800', // Orange
'#9c27b0', // Purple
'#00bcd4', // Cyan
),
);
foreach ($defaults as $option => $value) {
if (get_option($option) === false) {
add_option($option, $value);
}
}
}
add_action('after_setup_theme', 'roi_related_posts_default_options');

150
Inc/sanitize-functions.php Normal file
View File

@@ -0,0 +1,150 @@
<?php
/**
* Funciones de sanitización para el tema ROI
*
* Este archivo centraliza todas las funciones de sanitización utilizadas
* en el Customizer, panel de opciones y demás componentes del tema.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
if (!function_exists('roi_sanitize_checkbox')) {
/**
* Sanitiza valores de checkbox
*
* Convierte cualquier valor a boolean para asegurar que solo
* se guarden valores true/false en la base de datos.
*
* @param mixed $input Valor a sanitizar
* @return bool Valor sanitizado como boolean
* @since 1.0.0
*/
function roi_sanitize_checkbox($input) {
return (bool) $input;
}
}
if (!function_exists('roi_sanitize_select')) {
/**
* Sanitiza valores de select
*
* Verifica que el valor seleccionado existe en las opciones disponibles.
* Si no existe, retorna el valor por defecto del setting.
*
* @param mixed $input Valor a sanitizar
* @param object $setting Setting object del Customizer
* @return string Valor sanitizado
* @since 1.0.0
*/
function roi_sanitize_select($input, $setting) {
// Asegurar que el setting tiene las opciones disponibles
$choices = $setting->manager->get_control($setting->id)->choices;
// Retornar el input si es una opción válida, de lo contrario retornar el default
return (array_key_exists($input, $choices) ? $input : $setting->default);
}
}
if (!function_exists('roi_sanitize_css')) {
/**
* Sanitiza CSS
*
* Remueve scripts y código PHP potencialmente peligroso del CSS personalizado.
*
* @param string $css El string CSS a sanitizar
* @return string CSS sanitizado
* @since 1.0.0
*/
function roi_sanitize_css($css) {
// Remove <script> tags
$css = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $css);
// Remove potential PHP code
$css = preg_replace('#<\?php(.*?)\?>#is', '', $css);
return wp_strip_all_tags($css);
}
}
if (!function_exists('roi_sanitize_js')) {
/**
* Sanitiza JavaScript
*
* Remueve etiquetas script externas y código PHP del JavaScript personalizado.
*
* @param string $js El string JavaScript a sanitizar
* @return string JavaScript sanitizado
* @since 1.0.0
*/
function roi_sanitize_js($js) {
// Remove <script> tags if present
$js = preg_replace('#<script(.*?)>(.*?)</script>#is', '$2', $js);
// Remove potential PHP code
$js = preg_replace('#<\?php(.*?)\?>#is', '', $js);
return trim($js);
}
}
if (!function_exists('roi_sanitize_integer')) {
/**
* Sanitiza valores enteros
*
* Convierte el valor a un entero absoluto (no negativo).
*
* @param mixed $input Valor a sanitizar
* @return int Valor sanitizado como entero
* @since 1.0.0
*/
function roi_sanitize_integer($input) {
return absint($input);
}
}
if (!function_exists('roi_sanitize_text')) {
/**
* Sanitiza campos de texto
*
* Remueve etiquetas HTML y caracteres especiales del texto.
*
* @param string $input Valor a sanitizar
* @return string Texto sanitizado
* @since 1.0.0
*/
function roi_sanitize_text($input) {
return sanitize_text_field($input);
}
}
if (!function_exists('roi_sanitize_url')) {
/**
* Sanitiza URLs
*
* Valida y sanitiza URLs para asegurar que son válidas.
*
* @param string $input URL a sanitizar
* @return string URL sanitizada
* @since 1.0.0
*/
function roi_sanitize_url($input) {
return esc_url_raw($input);
}
}
if (!function_exists('roi_sanitize_html')) {
/**
* Sanitiza contenido HTML
*
* Permite etiquetas HTML seguras, removiendo scripts y código peligroso.
*
* @param string $input Contenido HTML a sanitizar
* @return string HTML sanitizado
* @since 1.0.0
*/
function roi_sanitize_html($input) {
return wp_kses_post($input);
}
}

115
Inc/search-disable.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
/**
* Desactivar funcionalidad de búsqueda nativa de WordPress
*
* Este archivo desactiva completamente la búsqueda nativa de WordPress.
* Las rutas de búsqueda retornarán 404.
*
* Comportamiento:
* - Rutas de búsqueda (ej. /search/ o /?s=query desde raíz) → 404
* - URLs válidas con parámetro ?s= → entregar página normal, ignorar parámetro
*
* @package ROI_Theme
* @since 1.0.0
* @link https://github.com/prime-leads-app/analisisdepreciosunitarios.com/issues/3
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Desactivar widget de búsqueda de WordPress
*
* Remueve el widget de búsqueda del admin para prevenir su uso.
*
* @since 1.0.0
*/
function roi_disable_search_widget() {
unregister_widget('WP_Widget_Search');
}
add_action('widgets_init', 'roi_disable_search_widget');
/**
* Bloquear queries de búsqueda
*
* Detecta búsquedas y las convierte en 404.
* Si es una URL válida con parámetro ?s=, ignora el parámetro y entrega la página normal.
*
* @since 1.0.0
* @param WP_Query $query La instancia de WP_Query.
*/
function roi_disable_search_queries($query) {
// Solo procesar en el frontend y en la query principal
if (is_admin() || !$query->is_main_query()) {
return;
}
// Si es una búsqueda
if ($query->is_search()) {
// Verificar si hay una página o post válido siendo solicitado
// Si solo es búsqueda (sin otra query var significativa), retornar 404
$query_vars = $query->query_vars;
// Si solo tiene el parámetro 's' y no está pidiendo una página específica
if (isset($query_vars['s']) &&
empty($query_vars['page_id']) &&
empty($query_vars['pagename']) &&
empty($query_vars['name']) &&
empty($query_vars['p'])) {
// Forzar 404
$query->set_404();
status_header(404);
nocache_headers();
}
}
}
add_action('pre_get_posts', 'roi_disable_search_queries', 10);
/**
* Remover enlaces de búsqueda del frontend
*
* Asegura que no haya formularios de búsqueda en el tema.
*
* @since 1.0.0
* @return string Cadena vacía.
*/
function roi_disable_search_form() {
return '';
}
add_filter('get_search_form', 'roi_disable_search_form');
/**
* Prevenir indexación de páginas de búsqueda
*
* Añade noindex a cualquier página de búsqueda que pueda escapar.
*
* @since 1.0.0
*/
function roi_noindex_search() {
if (is_search()) {
echo '<meta name="robots" content="noindex,nofollow">' . "\n";
}
}
add_action('wp_head', 'roi_noindex_search', 1);
/**
* Remover rewrite rules de búsqueda
*
* Elimina las reglas de reescritura relacionadas con búsqueda.
*
* @since 1.0.0
* @param array $rules Reglas de reescritura de WordPress.
* @return array Reglas modificadas sin búsqueda.
*/
function roi_remove_search_rewrite_rules($rules) {
foreach ($rules as $rule => $rewrite) {
if (preg_match('/search/', $rule)) {
unset($rules[$rule]);
}
}
return $rules;
}
add_filter('rewrite_rules_array', 'roi_remove_search_rewrite_rules');

200
Inc/seo.php Normal file
View File

@@ -0,0 +1,200 @@
<?php
/**
* SEO Optimizations and Rank Math Compatibility
*
* This file contains SEO-related theme functions that work
* seamlessly with Rank Math SEO plugin without conflicts.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Remove WordPress version from header and feeds
*
* Prevents disclosure of WordPress version number which could
* expose potential vulnerabilities. This is a common SEO best practice.
*
* @since 1.0.0
*/
function roi_remove_generator() {
return '';
}
remove_action('wp_head', 'wp_generator');
add_filter('the_generator', 'roi_remove_generator');
/**
* Remove RSD (Really Simple Discovery) link
*
* Removes unnecessary header link that's rarely needed.
*
* @since 1.0.0
*/
remove_action('wp_head', 'rsd_link');
/**
* Remove Windows Live Writer manifest
*
* Removes deprecated Microsoft Windows Live Writer support link.
*
* @since 1.0.0
*/
remove_action('wp_head', 'wlwmanifest_link');
/**
* Remove REST API link from header
*
* Note: Rank Math handles REST API headers, so we keep REST API
* itself enabled but remove the link tag from the header.
* This prevents exposing API endpoints unnecessarily.
*
* @since 1.0.0
*/
remove_action('wp_head', 'rest_output_link_wp_head');
/**
* Optimize robots.txt headers
*
* Ensures proper cache headers for robots.txt
*
* @since 1.0.0
*/
function roi_robots_header() {
if (is_robots()) {
header('Cache-Control: public, max-age=86400');
header('Expires: ' . gmdate('r', time() + 86400));
}
}
add_action('pre_handle_robots_txt', 'roi_robots_header');
/**
* Improve comment feed performance
*
* Disables post comments feed if not needed (can be re-enabled
* in theme options if required for client websites).
*
* @since 1.0.0
*/
// Note: Keep this commented out unless client specifically needs comment feeds
// remove_action('wp_head', 'feed_links', 2);
/**
* Clean up empty image alt attributes
*
* Encourages proper image SEO by highlighting missing alt text in admin
*
* @since 1.0.0
*/
function roi_admin_notice_missing_alt() {
if (!current_user_can('upload_files')) {
return;
}
// This is informational - actual alt text enforcement is better
// handled by Rank Math's image optimization features
}
/**
* Ensure wp_head() is properly closed before body
*
* This is called in header.php to ensure all SEO meta tags
* (from Rank Math and theme) are properly placed.
*
* @since 1.0.0
*/
function roi_seo_head_hooks() {
// This ensures proper hook execution order for Rank Math compatibility
do_action('roi_head_close');
}
/**
* ELIMINADO: roi_prefetch_external()
*
* Motivo: Fuentes Poppins ahora son self-hosted (Assets/fonts/)
* Ya no se necesita preconnect a Google Fonts
*
* @since 1.0.0 - Creado
* @since 1.1.0 - Eliminado (self-hosted fonts)
*/
/**
* Open Graph support for Rank Math compatibility
*
* Ensures theme doesn't output conflicting OG tags when Rank Math is active.
* Rank Math handles all Open Graph implementation.
*
* @since 1.0.0
*/
function roi_check_rank_math_active() {
return defined('RANK_MATH_VERSION');
}
/**
* Schema.org compatibility layer
*
* Provides basic schema support if Rank Math is not active.
* When Rank Math is active, it takes over all schema implementation.
*
* @since 1.0.0
*/
function roi_schema_fallback() {
// Only output schema if Rank Math is NOT active
if (roi_check_rank_math_active()) {
return;
}
// Basic organization schema fallback
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'name' => get_bloginfo('name'),
'url' => get_home_url(),
);
if (get_bloginfo('description')) {
$schema['description'] = get_bloginfo('description');
}
echo "\n" . '<!-- ROI Theme Basic Schema (Rank Math not active) -->' . "\n";
echo '<script type="application/ld+json">' . "\n";
echo wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
echo '</script>' . "\n";
}
add_action('wp_head', 'roi_schema_fallback', 20);
/**
* Security headers configuration
*
* Adds recommended security headers that also improve SEO
* (by indicating secure, well-maintained site)
*
* @since 1.0.0
*/
function roi_security_headers() {
// These headers improve trust signals for search engines
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
}
add_action('send_headers', 'roi_security_headers');
/**
* Ensure title tag support is active
*
* This is set in functions.php with add_theme_support('title-tag')
* but we verify it here to log any issues for debugging.
*
* @since 1.0.0
*/
function roi_verify_title_tag_support() {
if (!current_theme_supports('title-tag')) {
// Log warning if title-tag support is somehow disabled
error_log('Warning: ROI Theme title-tag support not properly initialized');
}
}
add_action('after_setup_theme', 'roi_verify_title_tag_support', 20);

127
Inc/social-share.php Normal file
View File

@@ -0,0 +1,127 @@
<?php
/**
* Social Share Buttons
*
* Funciones para mostrar botones de compartir en redes sociales
* en posts individuales. Utiliza URLs nativas sin dependencias de JavaScript.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Obtiene el HTML de los botones de compartir en redes sociales
*
* @param int $post_id ID del post (opcional, usa el post actual si no se proporciona)
* @return string HTML de los botones de compartir
*/
function roi_get_social_share_buttons( $post_id = 0 ) {
// Si no se proporciona post_id, usar el post actual
if ( ! $post_id ) {
$post_id = get_the_ID();
}
// Verificar que es un post válido
if ( ! $post_id ) {
return '';
}
// Obtener información del post
$post_title = get_the_title( $post_id );
$post_url = get_permalink( $post_id );
$post_url_encoded = urlencode( $post_url );
$post_title_encoded = urlencode( $post_title );
// URLs de compartir para cada red social
$facebook_url = 'https://www.facebook.com/sharer/sharer.php?u=' . $post_url_encoded;
$twitter_url = 'https://twitter.com/intent/tweet?url=' . $post_url_encoded . '&text=' . $post_title_encoded;
$linkedin_url = 'https://www.linkedin.com/shareArticle?mini=true&url=' . $post_url_encoded . '&title=' . $post_title_encoded;
$whatsapp_url = 'https://api.whatsapp.com/send?text=' . $post_title_encoded . '%20' . $post_url_encoded;
$email_url = 'mailto:?subject=' . $post_title_encoded . '&body=' . $post_url_encoded;
// Obtener texto de compartir desde BD (Clean Architecture)
$share_text = roi_get_component_setting( 'social-share', 'content', 'share_text', __( 'Compartir:', 'roi-theme' ) );
// Construir el HTML
ob_start();
?>
<!-- Share Buttons Section -->
<div class="social-share-section my-5 py-4 border-top">
<p class="mb-3 text-muted"><?php echo esc_html( $share_text ); ?></p>
<div class="d-flex gap-2 flex-wrap share-buttons">
<!-- Facebook -->
<a href="<?php echo esc_url( $facebook_url ); ?>"
class="btn btn-outline-primary btn-sm"
aria-label="<?php esc_attr_e( 'Compartir en Facebook', 'roi-theme' ); ?>"
target="_blank"
rel="noopener noreferrer">
<i class="bi bi-facebook"></i>
</a>
<!-- X (Twitter) -->
<a href="<?php echo esc_url( $twitter_url ); ?>"
class="btn btn-outline-dark btn-sm"
aria-label="<?php esc_attr_e( 'Compartir en X', 'roi-theme' ); ?>"
target="_blank"
rel="noopener noreferrer">
<i class="bi bi-twitter-x"></i>
</a>
<!-- LinkedIn -->
<a href="<?php echo esc_url( $linkedin_url ); ?>"
class="btn btn-outline-info btn-sm"
aria-label="<?php esc_attr_e( 'Compartir en LinkedIn', 'roi-theme' ); ?>"
target="_blank"
rel="noopener noreferrer">
<i class="bi bi-linkedin"></i>
</a>
<!-- WhatsApp -->
<a href="<?php echo esc_url( $whatsapp_url ); ?>"
class="btn btn-outline-success btn-sm"
aria-label="<?php esc_attr_e( 'Compartir en WhatsApp', 'roi-theme' ); ?>"
target="_blank"
rel="noopener noreferrer">
<i class="bi bi-whatsapp"></i>
</a>
<!-- Email -->
<a href="<?php echo esc_url( $email_url ); ?>"
class="btn btn-outline-secondary btn-sm"
aria-label="<?php esc_attr_e( 'Compartir por Email', 'roi-theme' ); ?>">
<i class="bi bi-envelope"></i>
</a>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Template tag para mostrar los botones de compartir
*
* Muestra los botones de compartir en redes sociales solo en posts individuales
* si la opción está habilitada en el panel de opciones del tema.
*
* @param int $post_id ID del post (opcional)
*/
function roi_display_social_share( $post_id = 0 ) {
// Solo mostrar en posts individuales
if ( ! is_single() ) {
return;
}
// Verificar si los botones de compartir están habilitados (Clean Architecture)
$is_enabled = roi_get_component_setting( 'social-share', 'visibility', 'is_enabled', true );
if ( !$is_enabled ) {
return;
}
// Mostrar los botones
echo roi_get_social_share_buttons( $post_id );
}

458
Inc/template-functions.php Normal file
View File

@@ -0,0 +1,458 @@
<?php
/**
* Template Functions
*
* Helper functions for theme templates.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Prints HTML with meta information for the current post-date/time and author.
*
* @since 1.0.0
*/
function roi_posted_on() {
$time_string = '<time class="entry-date published updated" datetime="%1$s">%2$s</time>';
if ( get_the_time( 'U' ) !== get_the_modified_time( 'U' ) ) {
$time_string = '<time class="entry-date published" datetime="%1$s">%2$s</time><time class="updated" datetime="%3$s">%4$s</time>';
}
$time_string = sprintf(
$time_string,
esc_attr( get_the_date( DATE_W3C ) ),
esc_html( get_the_date() ),
esc_attr( get_the_modified_date( DATE_W3C ) ),
esc_html( get_the_modified_date() )
);
$posted_on = sprintf(
/* translators: %s: post date. */
esc_html_x( 'Publicado el %s', 'post date', 'roi' ),
'<a href="' . esc_url( get_permalink() ) . '" rel="bookmark">' . $time_string . '</a>'
);
echo '<span class="posted-on">' . $posted_on . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Prints HTML with meta information for the current author.
*
* @since 1.0.0
*/
function roi_posted_by() {
$byline = sprintf(
/* translators: %s: post author. */
esc_html_x( 'por %s', 'post author', 'roi' ),
'<span class="author vcard"><a class="url fn n" href="' . esc_url( get_author_posts_url( get_the_author_meta( 'ID' ) ) ) . '">' . esc_html( get_the_author() ) . '</a></span>'
);
echo '<span class="byline"> ' . $byline . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Prints HTML with meta information for the categories, tags, and comments.
*
* @since 1.0.0
*/
function roi_entry_footer() {
// Hide category and tag text for pages.
if ( 'post' === get_post_type() ) {
/* translators: used between list items, there is a space after the comma */
$categories_list = get_the_category_list( esc_html__( ', ', 'roi' ) );
if ( $categories_list ) {
/* translators: 1: list of categories. */
printf( '<span class="cat-links">' . esc_html__( 'Categorías: %1$s', 'roi' ) . '</span>', $categories_list ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/* translators: used between list items, there is a space after the comma */
$tags_list = get_the_tag_list( '', esc_html_x( ', ', 'list item separator', 'roi' ) );
if ( $tags_list ) {
/* translators: 1: list of tags. */
printf( '<span class="tags-links">' . esc_html__( 'Etiquetas: %1$s', 'roi' ) . '</span>', $tags_list ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
if ( ! is_single() && ! post_password_required() && ( comments_open() || get_comments_number() ) ) {
echo '<span class="comments-link">';
comments_popup_link(
sprintf(
wp_kses(
/* translators: %s: post title */
__( 'Comentar<span class="screen-reader-text"> en %s</span>', 'roi' ),
array(
'span' => array(
'class' => array(),
),
)
),
wp_kses_post( get_the_title() )
)
);
echo '</span>';
}
edit_post_link(
sprintf(
wp_kses(
/* translators: %s: Name of current post. Only visible to screen readers */
__( 'Editar <span class="screen-reader-text">%s</span>', 'roi' ),
array(
'span' => array(
'class' => array(),
),
)
),
wp_kses_post( get_the_title() )
),
'<span class="edit-link">',
'</span>'
);
}
/**
* Custom excerpt length
*
* @since 1.0.0
* @param int $length Default excerpt length.
* @return int Modified excerpt length.
*/
function roi_excerpt_length( $length ) {
if ( is_admin() ) {
return $length;
}
return 40; // Change this to desired excerpt length.
}
add_filter( 'excerpt_length', 'roi_excerpt_length', 999 );
/**
* Custom excerpt more string
*
* @since 1.0.0
* @param string $more Default more string.
* @return string Modified more string.
*/
function roi_excerpt_more( $more ) {
if ( is_admin() ) {
return $more;
}
return '&hellip;';
}
add_filter( 'excerpt_more', 'roi_excerpt_more' );
/**
* Get custom excerpt by character count
*
* @since 1.0.0
* @param int $charlength Character length.
* @param string $more More string.
* @return string Custom excerpt.
*/
function roi_get_excerpt( $charlength = 200, $more = '...' ) {
$excerpt = get_the_excerpt();
$charlength++;
if ( mb_strlen( $excerpt ) > $charlength ) {
$subex = mb_substr( $excerpt, 0, $charlength - 5 );
$exwords = explode( ' ', $subex );
$excut = - ( mb_strlen( $exwords[ count( $exwords ) - 1 ] ) );
if ( $excut < 0 ) {
$excerpt = mb_substr( $subex, 0, $excut );
} else {
$excerpt = $subex;
}
$excerpt .= $more;
}
return $excerpt;
}
/**
* Get post thumbnail URL
*
* @since 1.0.0
* @param string $size Image size.
* @return string|false Thumbnail URL or false if not found.
*/
function roi_get_post_thumbnail_url( $size = 'full' ) {
if ( has_post_thumbnail() ) {
$thumb_id = get_post_thumbnail_id();
$thumb = wp_get_attachment_image_src( $thumb_id, $size );
if ( $thumb ) {
return $thumb[0];
}
}
return false;
}
/**
* Display breadcrumbs
*
* @since 1.0.0
* @param array $args Breadcrumb arguments.
*/
function roi_breadcrumbs( $args = array() ) {
// Default arguments.
$defaults = array(
'separator' => '<span class="separator">/</span>',
'home_label' => esc_html__( 'Inicio', 'roi' ),
'show_home' => true,
'show_current' => true,
'before' => '<nav class="breadcrumbs" aria-label="' . esc_attr__( 'Breadcrumb', 'roi' ) . '"><ol class="breadcrumb-list">',
'after' => '</ol></nav>',
'link_before' => '<li class="breadcrumb-item">',
'link_after' => '</li>',
);
$args = wp_parse_args( $args, $defaults );
// Don't display on homepage.
if ( is_front_page() ) {
return;
}
global $post;
echo $args['before']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
// Home link.
if ( $args['show_home'] ) {
echo $args['link_before'] . '<a href="' . esc_url( home_url( '/' ) ) . '">' . esc_html( $args['home_label'] ) . '</a>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['separator']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
if ( is_category() ) {
$category = get_queried_object();
if ( $category->parent ) {
$parent_cats = get_category_parents( $category->parent, true, $args['separator'] );
echo $args['link_before'] . $parent_cats . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo $args['link_before'] . '<span class="current">' . esc_html( single_cat_title( '', false ) ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} elseif ( is_single() && ! is_attachment() ) {
if ( get_post_type() !== 'post' ) {
$post_type = get_post_type_object( get_post_type() );
$post_type_archive = get_post_type_archive_link( get_post_type() );
if ( $post_type_archive ) {
echo $args['link_before'] . '<a href="' . esc_url( $post_type_archive ) . '">' . esc_html( $post_type->labels->name ) . '</a>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['separator']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
if ( $args['show_current'] ) {
echo $args['link_before'] . '<span class="current">' . esc_html( get_the_title() ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
} else {
$category = get_the_category();
if ( $category ) {
$category_values = array_values( $category );
$category_last = end( $category_values );
$category_parents = get_category_parents( $category_last->term_id, true, $args['separator'] );
echo $args['link_before'] . $category_parents . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
if ( $args['show_current'] ) {
echo $args['link_before'] . '<span class="current">' . esc_html( get_the_title() ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
} elseif ( is_page() && ! $post->post_parent ) {
if ( $args['show_current'] ) {
echo $args['link_before'] . '<span class="current">' . esc_html( get_the_title() ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
} elseif ( is_page() && $post->post_parent ) {
$parent_id = $post->post_parent;
$breadcrumbs = array();
while ( $parent_id ) {
$page = get_post( $parent_id );
$breadcrumbs[] = '<a href="' . esc_url( get_permalink( $page->ID ) ) . '">' . esc_html( get_the_title( $page->ID ) ) . '</a>';
$parent_id = $page->post_parent;
}
$breadcrumbs = array_reverse( $breadcrumbs );
foreach ( $breadcrumbs as $crumb ) {
echo $args['link_before'] . $crumb . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['separator']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
if ( $args['show_current'] ) {
echo $args['link_before'] . '<span class="current">' . esc_html( get_the_title() ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
} elseif ( is_search() ) {
echo $args['link_before'] . '<span class="current">' . esc_html__( 'Resultados de búsqueda para: ', 'roi' ) . get_search_query() . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} elseif ( is_tag() ) {
echo $args['link_before'] . '<span class="current">' . esc_html__( 'Etiqueta: ', 'roi' ) . esc_html( single_tag_title( '', false ) ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} elseif ( is_author() ) {
$author = get_queried_object();
echo $args['link_before'] . '<span class="current">' . esc_html__( 'Autor: ', 'roi' ) . esc_html( $author->display_name ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} elseif ( is_day() ) {
echo $args['link_before'] . '<a href="' . esc_url( get_year_link( get_the_time( 'Y' ) ) ) . '">' . esc_html( get_the_time( 'Y' ) ) . '</a>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['separator']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['link_before'] . '<a href="' . esc_url( get_month_link( get_the_time( 'Y' ), get_the_time( 'm' ) ) ) . '">' . esc_html( get_the_time( 'F' ) ) . '</a>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['separator']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['link_before'] . '<span class="current">' . esc_html( get_the_time( 'd' ) ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} elseif ( is_month() ) {
echo $args['link_before'] . '<a href="' . esc_url( get_year_link( get_the_time( 'Y' ) ) ) . '">' . esc_html( get_the_time( 'Y' ) ) . '</a>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['separator']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['link_before'] . '<span class="current">' . esc_html( get_the_time( 'F' ) ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} elseif ( is_year() ) {
echo $args['link_before'] . '<span class="current">' . esc_html( get_the_time( 'Y' ) ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} elseif ( is_404() ) {
echo $args['link_before'] . '<span class="current">' . esc_html__( 'Error 404', 'roi' ) . '</span>' . $args['link_after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo $args['after']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Add custom body classes
*
* @since 1.0.0
* @param array $classes Existing body classes.
* @return array Modified body classes.
*/
function roi_body_classes( $classes ) {
// Add class if sidebar is active.
if ( is_active_sidebar( 'sidebar-1' ) ) {
$classes[] = 'has-sidebar';
} else {
$classes[] = 'no-sidebar';
}
// Add class if it's a singular post/page.
if ( is_singular() ) {
$classes[] = 'singular';
}
// Add class for specific post formats.
if ( is_singular() && has_post_format() ) {
$post_format = get_post_format();
$classes[] = 'post-format-' . $post_format;
}
return $classes;
}
add_filter( 'body_class', 'roi_body_classes' );
/**
* Add custom post classes
*
* @since 1.0.0
* @param array $classes Existing post classes.
* @return array Modified post classes.
*/
function roi_post_classes( $classes ) {
// Add class if post has thumbnail.
if ( has_post_thumbnail() ) {
$classes[] = 'has-post-thumbnail';
} else {
$classes[] = 'no-post-thumbnail';
}
return $classes;
}
add_filter( 'post_class', 'roi_post_classes' );
/**
* Sanitize SVG uploads
*
* @since 1.0.0
* @param array $mimes Allowed mime types.
* @return array Modified mime types.
*/
function roi_add_svg_mime_types( $mimes ) {
$mimes['svg'] = 'image/svg+xml';
$mimes['svgz'] = 'image/svg+xml';
return $mimes;
}
add_filter( 'upload_mimes', 'roi_add_svg_mime_types' );
/**
* Pagination for archive pages
*
* @since 1.0.0
* @param array $args Pagination arguments.
*/
function roi_pagination( $args = array() ) {
global $wp_query;
// Don't print empty markup if there's only one page.
if ( $wp_query->max_num_pages < 2 ) {
return;
}
$defaults = array(
'mid_size' => 2,
'prev_text' => esc_html__( '&larr; Anterior', 'roi' ),
'next_text' => esc_html__( 'Siguiente &rarr;', 'roi' ),
'screen_reader_text' => esc_html__( 'Navegación de entradas', 'roi' ),
'type' => 'list',
'current' => max( 1, get_query_var( 'paged' ) ),
);
$args = wp_parse_args( $args, $defaults );
// Make sure we get a string back. Plain is the next best thing.
if ( isset( $args['type'] ) && 'array' === $args['type'] ) {
$args['type'] = 'plain';
}
echo '<nav class="pagination" aria-label="' . esc_attr( $args['screen_reader_text'] ) . '">';
echo wp_kses_post( paginate_links( $args ) );
echo '</nav>';
}
/**
* Get footer column Bootstrap class
*
* Returns the appropriate Bootstrap grid classes for footer columns
* based on the column number. Supports configurable widths and
* responsive breakpoints.
*
* @since 1.0.0
* @param int $column Column number (1-4).
* @return string Bootstrap column classes.
*/
function roi_get_footer_column_class( $column = 1 ) {
// Default configuration: Equal width columns (3 columns each on desktop).
// You can customize these classes per column as needed.
$column_classes = array(
1 => 'col-12 col-md-6 col-lg-3', // Column 1: Full width mobile, half tablet, quarter desktop.
2 => 'col-12 col-md-6 col-lg-3', // Column 2: Full width mobile, half tablet, quarter desktop.
3 => 'col-12 col-md-6 col-lg-3', // Column 3: Full width mobile, half tablet, quarter desktop.
4 => 'col-12 col-md-6 col-lg-3', // Column 4: Full width mobile, half tablet, quarter desktop.
);
/**
* Filter footer column classes
*
* Allows customization of footer column widths via filter.
*
* @since 1.0.0
* @param array $column_classes Array of column classes.
*/
$column_classes = apply_filters( 'roi_footer_column_classes', $column_classes );
// Return the class for the specified column, or default to col-12 if not found.
return isset( $column_classes[ $column ] ) ? $column_classes[ $column ] : 'col-12';
}

544
Inc/template-tags.php Normal file
View File

@@ -0,0 +1,544 @@
<?php
/**
* Template Tags
*
* Reusable template tags for theme templates.
*
* @package ROI_Theme
* @since 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Display the site logo
*
* @since 1.0.0
* @param array $args Logo arguments.
*/
function roi_site_logo( $args = array() ) {
$defaults = array(
'class' => 'site-logo',
'width' => 200,
);
$args = wp_parse_args( $args, $defaults );
if ( has_custom_logo() ) {
$custom_logo_id = get_theme_mod( 'custom_logo' );
$logo = wp_get_attachment_image_src( $custom_logo_id, 'full' );
if ( $logo ) {
printf(
'<a href="%1$s" class="%2$s" rel="home">
<img src="%3$s" alt="%4$s" width="%5$d">
</a>',
esc_url( home_url( '/' ) ),
esc_attr( $args['class'] ),
esc_url( $logo[0] ),
esc_attr( get_bloginfo( 'name' ) ),
absint( $args['width'] )
);
}
} else {
printf(
'<a href="%1$s" class="%2$s site-title" rel="home">%3$s</a>',
esc_url( home_url( '/' ) ),
esc_attr( $args['class'] ),
esc_html( get_bloginfo( 'name' ) )
);
}
}
/**
* Display the site description
*
* @since 1.0.0
* @param array $args Description arguments.
*/
function roi_site_description( $args = array() ) {
$description = get_bloginfo( 'description', 'display' );
if ( ! $description ) {
return;
}
$defaults = array(
'class' => 'site-description',
'tag' => 'p',
);
$args = wp_parse_args( $args, $defaults );
printf(
'<%1$s class="%2$s">%3$s</%1$s>',
esc_attr( $args['tag'] ),
esc_attr( $args['class'] ),
esc_html( $description )
);
}
/**
* Display post meta information
*
* @since 1.0.0
* @param array $args Meta arguments.
*/
function roi_post_meta( $args = array() ) {
$defaults = array(
'show_date' => true,
'show_author' => true,
'show_comments' => true,
'show_category' => true,
'class' => 'entry-meta',
);
$args = wp_parse_args( $args, $defaults );
echo '<div class="' . esc_attr( $args['class'] ) . '">';
if ( $args['show_date'] ) {
roi_posted_on();
}
if ( $args['show_author'] ) {
roi_posted_by();
}
if ( $args['show_category'] && 'post' === get_post_type() ) {
$categories_list = get_the_category_list( esc_html__( ', ', 'roi' ) );
if ( $categories_list ) {
printf(
'<span class="cat-links">%s</span>',
$categories_list // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
}
if ( $args['show_comments'] && ! post_password_required() && ( comments_open() || get_comments_number() ) ) {
echo '<span class="comments-link">';
comments_popup_link(
sprintf(
wp_kses(
/* translators: %s: post title */
__( '0 comentarios<span class="screen-reader-text"> en %s</span>', 'roi' ),
array(
'span' => array(
'class' => array(),
),
)
),
wp_kses_post( get_the_title() )
),
sprintf(
wp_kses(
/* translators: %s: post title */
__( '1 comentario<span class="screen-reader-text"> en %s</span>', 'roi' ),
array(
'span' => array(
'class' => array(),
),
)
),
wp_kses_post( get_the_title() )
),
sprintf(
wp_kses(
/* translators: %s: post title */
__( '% comentarios<span class="screen-reader-text"> en %s</span>', 'roi' ),
array(
'span' => array(
'class' => array(),
),
)
),
wp_kses_post( get_the_title() )
)
);
echo '</span>';
}
echo '</div>';
}
/**
* Display post thumbnail
*
* @since 1.0.0
* @param array $args Thumbnail arguments.
*/
function roi_post_thumbnail( $args = array() ) {
if ( ! has_post_thumbnail() ) {
return;
}
$defaults = array(
'size' => 'large',
'class' => 'post-thumbnail',
'link' => true,
);
$args = wp_parse_args( $args, $defaults );
if ( $args['link'] && ! is_single() ) {
?>
<a class="<?php echo esc_attr( $args['class'] ); ?>" href="<?php the_permalink(); ?>" aria-hidden="true" tabindex="-1">
<?php the_post_thumbnail( $args['size'] ); ?>
</a>
<?php
} else {
?>
<div class="<?php echo esc_attr( $args['class'] ); ?>">
<?php the_post_thumbnail( $args['size'] ); ?>
</div>
<?php
}
}
/**
* Display read more link
*
* @since 1.0.0
* @param array $args Read more arguments.
*/
function roi_read_more( $args = array() ) {
$defaults = array(
'text' => esc_html__( 'Leer más', 'roi' ),
'class' => 'read-more',
);
$args = wp_parse_args( $args, $defaults );
printf(
'<a href="%1$s" class="%2$s">%3$s</a>',
esc_url( get_permalink() ),
esc_attr( $args['class'] ),
esc_html( $args['text'] )
);
}
/**
* Display social sharing buttons
*
* @since 1.0.0
* @param array $args Share arguments.
*/
function roi_social_share( $args = array() ) {
$defaults = array(
'title' => esc_html__( 'Compartir:', 'roi' ),
'networks' => array( 'facebook', 'twitter', 'linkedin', 'whatsapp' ),
'class' => 'social-share',
);
$args = wp_parse_args( $args, $defaults );
$post_title = get_the_title();
$post_url = get_permalink();
echo '<div class="' . esc_attr( $args['class'] ) . '">';
if ( $args['title'] ) {
echo '<span class="share-title">' . esc_html( $args['title'] ) . '</span>';
}
echo '<ul class="share-buttons">';
foreach ( $args['networks'] as $network ) {
$share_url = '';
$icon_class = 'share-' . $network;
switch ( $network ) {
case 'facebook':
$share_url = 'https://www.facebook.com/sharer/sharer.php?u=' . rawurlencode( $post_url );
break;
case 'twitter':
$share_url = 'https://twitter.com/intent/tweet?text=' . rawurlencode( $post_title ) . '&url=' . rawurlencode( $post_url );
break;
case 'linkedin':
$share_url = 'https://www.linkedin.com/shareArticle?mini=true&url=' . rawurlencode( $post_url ) . '&title=' . rawurlencode( $post_title );
break;
case 'whatsapp':
$share_url = 'https://api.whatsapp.com/send?text=' . rawurlencode( $post_title . ' ' . $post_url );
break;
}
if ( $share_url ) {
printf(
'<li class="share-item">
<a href="%1$s" class="%2$s" target="_blank" rel="noopener noreferrer" aria-label="%3$s">
<span class="screen-reader-text">%4$s</span>
</a>
</li>',
esc_url( $share_url ),
esc_attr( $icon_class ),
/* translators: %s: social network name */
esc_attr( sprintf( __( 'Compartir en %s', 'roi' ), ucfirst( $network ) ) ),
esc_html( ucfirst( $network ) )
);
}
}
echo '</ul>';
echo '</div>';
}
/**
* Display author bio box
*
* @since 1.0.0
* @param array $args Author bio arguments.
*/
function roi_author_bio( $args = array() ) {
// Only show on single posts.
if ( ! is_single() ) {
return;
}
$defaults = array(
'class' => 'author-bio',
'show_avatar' => true,
'avatar_size' => 80,
'show_archive' => true,
);
$args = wp_parse_args( $args, $defaults );
$author_id = get_the_author_meta( 'ID' );
$author_name = get_the_author();
$author_description = get_the_author_meta( 'description' );
if ( empty( $author_description ) ) {
return;
}
echo '<div class="' . esc_attr( $args['class'] ) . '">';
if ( $args['show_avatar'] ) {
echo '<div class="author-avatar">';
echo get_avatar( $author_id, $args['avatar_size'] );
echo '</div>';
}
echo '<div class="author-info">';
echo '<h3 class="author-name">' . esc_html( $author_name ) . '</h3>';
echo '<p class="author-description">' . wp_kses_post( $author_description ) . '</p>';
if ( $args['show_archive'] ) {
printf(
'<a href="%1$s" class="author-link">%2$s</a>',
esc_url( get_author_posts_url( $author_id ) ),
/* translators: %s: author name */
esc_html( sprintf( __( 'Ver todos los artículos de %s', 'roi' ), $author_name ) )
);
}
echo '</div>';
echo '</div>';
}
/**
* Display related posts
*
* @since 1.0.0
* @param array $args Related posts arguments.
*/
function roi_related_posts( $args = array() ) {
// Only show on single posts.
if ( ! is_single() || 'post' !== get_post_type() ) {
return;
}
$defaults = array(
'title' => esc_html__( 'Artículos relacionados', 'roi' ),
'posts_per_page' => 3,
'order' => 'DESC',
'orderby' => 'date',
'class' => 'related-posts',
);
$args = wp_parse_args( $args, $defaults );
// Get current post categories.
$categories = get_the_category();
if ( empty( $categories ) ) {
return;
}
$category_ids = array();
foreach ( $categories as $category ) {
$category_ids[] = $category->term_id;
}
// Query related posts.
$related_query = new WP_Query(
array(
'category__in' => $category_ids,
'post__not_in' => array( get_the_ID() ),
'posts_per_page' => $args['posts_per_page'],
'order' => $args['order'],
'orderby' => $args['orderby'],
'ignore_sticky_posts' => 1,
)
);
if ( ! $related_query->have_posts() ) {
return;
}
echo '<aside class="' . esc_attr( $args['class'] ) . '">';
if ( $args['title'] ) {
echo '<h3 class="related-posts-title">' . esc_html( $args['title'] ) . '</h3>';
}
echo '<div class="related-posts-grid">';
while ( $related_query->have_posts() ) {
$related_query->the_post();
?>
<article class="related-post">
<?php if ( has_post_thumbnail() ) : ?>
<a href="<?php the_permalink(); ?>" class="related-post-thumbnail">
<?php the_post_thumbnail( 'medium' ); ?>
</a>
<?php endif; ?>
<h4 class="related-post-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h4>
<div class="related-post-meta">
<time datetime="<?php echo esc_attr( get_the_date( DATE_W3C ) ); ?>">
<?php echo esc_html( get_the_date() ); ?>
</time>
</div>
</article>
<?php
}
echo '</div>';
echo '</aside>';
wp_reset_postdata();
}
/**
* Display table of contents for long posts
*
* @since 1.0.0
* @param array $args TOC arguments.
*/
function roi_table_of_contents( $args = array() ) {
global $post;
if ( ! is_single() || ! isset( $post->post_content ) ) {
return;
}
$defaults = array(
'title' => esc_html__( 'Tabla de contenidos', 'roi' ),
'class' => 'table-of-contents',
'min_headings' => 3,
'heading_levels' => array( 'h2', 'h3' ),
);
$args = wp_parse_args( $args, $defaults );
// Extract headings from content.
$content = $post->post_content;
preg_match_all( '/<(' . implode( '|', $args['heading_levels'] ) . ')[^>]*>(.*?)<\/\1>/i', $content, $matches, PREG_SET_ORDER );
if ( count( $matches ) < $args['min_headings'] ) {
return;
}
echo '<nav class="' . esc_attr( $args['class'] ) . '">';
if ( $args['title'] ) {
echo '<h2 class="toc-title">' . esc_html( $args['title'] ) . '</h2>';
}
echo '<ol class="toc-list">';
foreach ( $matches as $index => $heading ) {
$heading_text = wp_strip_all_tags( $heading[2] );
$heading_id = sanitize_title( $heading_text ) . '-' . $index;
printf(
'<li class="toc-item toc-%1$s"><a href="#%2$s">%3$s</a></li>',
esc_attr( $heading[1] ),
esc_attr( $heading_id ),
esc_html( $heading_text )
);
}
echo '</ol>';
echo '</nav>';
}
/**
* Display cookie consent notice
*
* @since 1.0.0
* @param array $args Cookie notice arguments.
*/
function roi_cookie_notice( $args = array() ) {
// Check if cookie consent has been given.
if ( isset( $_COOKIE['roi_cookie_consent'] ) ) {
return;
}
$defaults = array(
'message' => esc_html__( 'Este sitio utiliza cookies para mejorar su experiencia. Al continuar navegando, acepta nuestro uso de cookies.', 'roi' ),
'accept_text' => esc_html__( 'Aceptar', 'roi' ),
'learn_more' => esc_html__( 'Más información', 'roi' ),
'policy_url' => get_privacy_policy_url(),
'class' => 'cookie-notice',
);
$args = wp_parse_args( $args, $defaults );
?>
<div class="<?php echo esc_attr( $args['class'] ); ?>" role="alert" aria-live="polite">
<div class="cookie-notice-content">
<p><?php echo esc_html( $args['message'] ); ?></p>
<div class="cookie-notice-actions">
<button type="button" class="cookie-notice-accept" id="cookie-notice-accept">
<?php echo esc_html( $args['accept_text'] ); ?>
</button>
<?php if ( $args['policy_url'] ) : ?>
<a href="<?php echo esc_url( $args['policy_url'] ); ?>" class="cookie-notice-learn-more">
<?php echo esc_html( $args['learn_more'] ); ?>
</a>
<?php endif; ?>
</div>
</div>
</div>
<?php
}
/**
* Calcula tiempo de lectura estimado
*
* @since 1.0.0
* @return string Tiempo de lectura (ej: "5 min de lectura")
*/
function roi_get_reading_time() {
$content = get_post_field('post_content', get_the_ID());
$word_count = str_word_count(strip_tags($content));
$reading_time = ceil($word_count / 200); // 200 palabras por minuto
if ($reading_time < 1) {
$reading_time = 1;
}
return sprintf(_n('%s min de lectura', '%s min de lectura', $reading_time, 'roi-theme'), $reading_time);
}