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:
151
Inc/adsense-delay.php
Normal file
151
Inc/adsense-delay.php
Normal 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
316
Inc/apu-tables.php
Normal 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
81
Inc/category-badge.php
Normal 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
253
Inc/comments-disable.php
Normal 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
367
Inc/critical-css.php
Normal 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
511
Inc/enqueue-scripts.php
Normal 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
434
Inc/featured-image.php
Normal 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
500
Inc/image-optimization.php
Normal 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
200
Inc/nav-walker.php
Normal 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
588
Inc/performance.php
Normal 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
294
Inc/related-posts.php
Normal 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
150
Inc/sanitize-functions.php
Normal 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
115
Inc/search-disable.php
Normal 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
200
Inc/seo.php
Normal 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
127
Inc/social-share.php
Normal 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
458
Inc/template-functions.php
Normal 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 '…';
|
||||
}
|
||||
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__( '← Anterior', 'roi' ),
|
||||
'next_text' => esc_html__( 'Siguiente →', '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
544
Inc/template-tags.php
Normal 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user