Refactor: Reorganizar repositorio - Solo tema WordPress

Se movió el repositorio git desde la raíz de WordPress a la carpeta del tema.
Este commit limpia todos los archivos de WordPress del historial de tracking
y mantiene únicamente los archivos del tema apus-theme.

Cambios:
- Eliminado tracking de archivos de WordPress core
- Mantenido solo archivos del tema (97 archivos)
- Actualizado .gitignore para excluir carpetas de desarrollo
- Historial de commits anteriores se mantiene intacto

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-09 09:15:47 -06:00
parent 861267e699
commit bbc6ed2c98
24226 changed files with 97 additions and 5056856 deletions

View File

@@ -0,0 +1,394 @@
<?php
/**
* Theme Options Usage Examples
*
* This file contains examples of how to use theme options throughout the theme.
* DO NOT include this file in functions.php - it's for reference only.
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* EXAMPLE 1: Using options in header.php
*/
function example_display_logo() {
$logo_url = apus_get_logo_url();
if ($logo_url) {
?>
<a href="<?php echo esc_url(home_url('/')); ?>" class="custom-logo-link">
<img src="<?php echo esc_url($logo_url); ?>" alt="<?php bloginfo('name'); ?>" class="custom-logo" />
</a>
<?php
} else {
?>
<h1 class="site-title">
<a href="<?php echo esc_url(home_url('/')); ?>"><?php bloginfo('name'); ?></a>
</h1>
<?php
}
}
/**
* EXAMPLE 2: Displaying breadcrumbs
*/
function example_show_breadcrumbs() {
if (apus_show_breadcrumbs() && !is_front_page()) {
$separator = apus_get_breadcrumb_separator();
echo '<nav class="breadcrumbs">';
echo '<a href="' . esc_url(home_url('/')) . '">Home</a>';
echo ' ' . esc_html($separator) . ' ';
if (is_single()) {
the_category(' ' . esc_html($separator) . ' ');
echo ' ' . esc_html($separator) . ' ';
the_title();
} elseif (is_category()) {
single_cat_title();
}
echo '</nav>';
}
}
/**
* EXAMPLE 3: Customizing excerpt
*/
function example_custom_excerpt_length($length) {
return apus_get_excerpt_length();
}
add_filter('excerpt_length', 'example_custom_excerpt_length');
function example_custom_excerpt_more($more) {
return apus_get_excerpt_more();
}
add_filter('excerpt_more', 'example_custom_excerpt_more');
/**
* EXAMPLE 4: Displaying related posts in single.php
*/
function example_display_related_posts() {
if (apus_show_related_posts() && is_single()) {
$count = apus_get_related_posts_count();
$taxonomy = apus_get_related_posts_taxonomy();
$title = apus_get_related_posts_title();
// Get related posts
$post_id = get_the_ID();
$args = array(
'posts_per_page' => $count,
'post__not_in' => array($post_id),
);
if ($taxonomy === 'category') {
$categories = wp_get_post_categories($post_id);
if ($categories) {
$args['category__in'] = $categories;
}
} elseif ($taxonomy === 'tag') {
$tags = wp_get_post_tags($post_id, array('fields' => 'ids'));
if ($tags) {
$args['tag__in'] = $tags;
}
}
$related = new WP_Query($args);
if ($related->have_posts()) {
?>
<div class="related-posts">
<h3><?php echo esc_html($title); ?></h3>
<div class="related-posts-grid">
<?php
while ($related->have_posts()) {
$related->the_post();
?>
<article class="related-post-item">
<?php if (has_post_thumbnail()) : ?>
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail('apus-thumbnail'); ?>
</a>
<?php endif; ?>
<h4>
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h4>
<div class="post-meta">
<time datetime="<?php echo get_the_date('c'); ?>">
<?php echo get_the_date(apus_get_date_format()); ?>
</time>
</div>
</article>
<?php
}
wp_reset_postdata();
?>
</div>
</div>
<?php
}
}
}
/**
* EXAMPLE 5: Conditional comments display
*/
function example_maybe_show_comments() {
if (is_single() && apus_comments_enabled_for_posts()) {
comments_template();
} elseif (is_page() && apus_comments_enabled_for_pages()) {
comments_template();
}
}
/**
* EXAMPLE 6: Featured image on single posts
*/
function example_display_featured_image() {
if (is_single() && apus_show_featured_image_single() && has_post_thumbnail()) {
?>
<div class="post-thumbnail">
<?php the_post_thumbnail('apus-featured-large'); ?>
</div>
<?php
}
}
/**
* EXAMPLE 7: Author box on single posts
*/
function example_display_author_box() {
if (is_single() && apus_show_author_box()) {
$author_id = get_the_author_meta('ID');
?>
<div class="author-box">
<div class="author-avatar">
<?php echo get_avatar($author_id, 80); ?>
</div>
<div class="author-info">
<h4 class="author-name"><?php the_author(); ?></h4>
<p class="author-bio"><?php the_author_meta('description'); ?></p>
<a href="<?php echo get_author_posts_url($author_id); ?>" class="author-link">
<?php _e('View all posts', 'apus-theme'); ?>
</a>
</div>
</div>
<?php
}
}
/**
* EXAMPLE 8: Social media links in footer
*/
function example_display_social_links() {
$social_links = apus_get_social_links();
// Filter out empty links
$social_links = array_filter($social_links);
if (!empty($social_links)) {
?>
<div class="social-links">
<?php foreach ($social_links as $network => $url) : ?>
<a href="<?php echo esc_url($url); ?>"
target="_blank"
rel="noopener noreferrer"
class="social-link social-<?php echo esc_attr($network); ?>">
<span class="screen-reader-text"><?php echo ucfirst($network); ?></span>
<i class="icon-<?php echo esc_attr($network); ?>"></i>
</a>
<?php endforeach; ?>
</div>
<?php
}
}
/**
* EXAMPLE 9: Copyright text in footer
*/
function example_display_copyright() {
$copyright = apus_get_copyright_text();
if ($copyright) {
echo '<div class="copyright">' . wp_kses_post($copyright) . '</div>';
}
}
/**
* EXAMPLE 10: Custom CSS in header
*/
function example_add_custom_css() {
$custom_css = apus_get_custom_css();
if ($custom_css) {
echo '<style type="text/css">' . "\n";
echo strip_tags($custom_css);
echo "\n</style>\n";
}
}
add_action('wp_head', 'example_add_custom_css', 100);
/**
* EXAMPLE 11: Custom JS in header
*/
function example_add_custom_js_header() {
$custom_js = apus_get_custom_js_header();
if ($custom_js) {
echo '<script type="text/javascript">' . "\n";
echo $custom_js;
echo "\n</script>\n";
}
}
add_action('wp_head', 'example_add_custom_js_header', 100);
/**
* EXAMPLE 12: Custom JS in footer
*/
function example_add_custom_js_footer() {
$custom_js = apus_get_custom_js_footer();
if ($custom_js) {
echo '<script type="text/javascript">' . "\n";
echo $custom_js;
echo "\n</script>\n";
}
}
add_action('wp_footer', 'example_add_custom_js_footer', 100);
/**
* EXAMPLE 13: Posts per page for archives
*/
function example_set_archive_posts_per_page($query) {
if ($query->is_archive() && !is_admin() && $query->is_main_query()) {
$posts_per_page = apus_get_archive_posts_per_page();
$query->set('posts_per_page', $posts_per_page);
}
}
add_action('pre_get_posts', 'example_set_archive_posts_per_page');
/**
* EXAMPLE 14: Performance optimizations
*/
function example_apply_performance_settings() {
// Remove emoji scripts
if (apus_is_performance_enabled('remove_emoji')) {
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('wp_print_styles', 'print_emoji_styles');
}
// Remove embeds
if (apus_is_performance_enabled('remove_embeds')) {
wp_deregister_script('wp-embed');
}
// Remove Dashicons for non-logged users
if (apus_is_performance_enabled('remove_dashicons') && !is_user_logged_in()) {
wp_deregister_style('dashicons');
}
}
add_action('wp_enqueue_scripts', 'example_apply_performance_settings', 100);
/**
* EXAMPLE 15: Lazy loading images
*/
function example_add_lazy_loading($attr, $attachment, $size) {
if (apus_is_lazy_loading_enabled()) {
$attr['loading'] = 'lazy';
}
return $attr;
}
add_filter('wp_get_attachment_image_attributes', 'example_add_lazy_loading', 10, 3);
/**
* EXAMPLE 16: Layout classes based on settings
*/
function example_get_layout_class() {
$layout = 'right-sidebar'; // default
if (is_single()) {
$layout = apus_get_default_post_layout();
} elseif (is_page()) {
$layout = apus_get_default_page_layout();
}
return 'layout-' . $layout;
}
/**
* EXAMPLE 17: Display post meta conditionally
*/
function example_display_post_meta() {
if (!apus_get_option('show_post_meta', true)) {
return;
}
?>
<div class="post-meta">
<span class="post-date">
<time datetime="<?php echo get_the_date('c'); ?>">
<?php echo get_the_date(apus_get_date_format()); ?>
</time>
</span>
<span class="post-author">
<?php the_author(); ?>
</span>
<?php if (apus_get_option('show_post_categories', true)) : ?>
<span class="post-categories">
<?php the_category(', '); ?>
</span>
<?php endif; ?>
</div>
<?php
}
/**
* EXAMPLE 18: Display post tags conditionally
*/
function example_display_post_tags() {
if (is_single() && apus_get_option('show_post_tags', true)) {
the_tags('<div class="post-tags">', ', ', '</div>');
}
}
/**
* EXAMPLE 19: Get all options (for debugging)
*/
function example_debug_all_options() {
if (current_user_can('manage_options') && isset($_GET['debug_options'])) {
$all_options = apus_get_all_options();
echo '<pre>';
print_r($all_options);
echo '</pre>';
}
}
add_action('wp_footer', 'example_debug_all_options');
/**
* EXAMPLE 20: Check if specific feature is enabled
*/
function example_check_feature() {
// Multiple ways to check boolean options
// Method 1: Using helper function
if (apus_is_option_enabled('enable_breadcrumbs')) {
// Breadcrumbs are enabled
}
// Method 2: Using get_option with default
if (apus_get_option('enable_related_posts', true)) {
// Related posts are enabled
}
// Method 3: Direct check
$options = apus_get_all_options();
if (isset($options['enable_lazy_loading']) && $options['enable_lazy_loading']) {
// Lazy loading is enabled
}
}

237
inc/admin/options-api.php Normal file
View File

@@ -0,0 +1,237 @@
<?php
/**
* Theme Options Settings API
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Register all theme settings
*/
function apus_register_settings() {
// Register main options group
register_setting(
'apus_theme_options_group',
'apus_theme_options',
array(
'sanitize_callback' => 'apus_sanitize_options',
'default' => apus_get_default_options(),
)
);
// General Settings Section
add_settings_section(
'apus_general_section',
__('General Settings', 'apus-theme'),
'apus_general_section_callback',
'apus-theme-options'
);
// Content Settings Section
add_settings_section(
'apus_content_section',
__('Content Settings', 'apus-theme'),
'apus_content_section_callback',
'apus-theme-options'
);
// Performance Settings Section
add_settings_section(
'apus_performance_section',
__('Performance Settings', 'apus-theme'),
'apus_performance_section_callback',
'apus-theme-options'
);
// Related Posts Settings Section
add_settings_section(
'apus_related_posts_section',
__('Related Posts Settings', 'apus-theme'),
'apus_related_posts_section_callback',
'apus-theme-options'
);
// Social Share Settings Section
add_settings_section(
'apus_social_share_section',
__('Social Share Buttons', 'apus-theme'),
'apus_social_share_section_callback',
'apus-theme-options'
);
}
add_action('admin_init', 'apus_register_settings');
/**
* Get default options
*
* @return array
*/
function apus_get_default_options() {
return array(
// General
'site_logo' => 0,
'site_favicon' => 0,
'enable_breadcrumbs' => true,
'breadcrumb_separator' => '>',
'date_format' => 'd/m/Y',
'time_format' => 'H:i',
'copyright_text' => sprintf(__('&copy; %s %s. All rights reserved.', 'apus-theme'), date('Y'), get_bloginfo('name')),
'social_facebook' => '',
'social_twitter' => '',
'social_instagram' => '',
'social_linkedin' => '',
'social_youtube' => '',
// Content
'excerpt_length' => 55,
'excerpt_more' => '...',
'default_post_layout' => 'right-sidebar',
'default_page_layout' => 'right-sidebar',
'archive_posts_per_page' => 10,
'show_featured_image_single' => true,
'show_author_box' => true,
'enable_comments_posts' => true,
'enable_comments_pages' => false,
'show_post_meta' => true,
'show_post_tags' => true,
'show_post_categories' => true,
// Performance
'enable_lazy_loading' => true,
'performance_remove_emoji' => true,
'performance_remove_embeds' => false,
'performance_remove_dashicons' => true,
'performance_defer_js' => false,
'performance_minify_html' => false,
'performance_disable_gutenberg' => false,
// Related Posts
'enable_related_posts' => true,
'related_posts_count' => 3,
'related_posts_taxonomy' => 'category',
'related_posts_title' => __('Related Posts', 'apus-theme'),
'related_posts_columns' => 3,
// Social Share Buttons
'apus_enable_share_buttons' => '1',
'apus_share_text' => __('Compartir:', 'apus-theme'),
// Advanced
'custom_css' => '',
'custom_js_header' => '',
'custom_js_footer' => '',
);
}
/**
* Section Callbacks
*/
function apus_general_section_callback() {
echo '<p>' . __('Configure general theme settings including logo, branding, and social media.', 'apus-theme') . '</p>';
}
function apus_content_section_callback() {
echo '<p>' . __('Configure content display settings for posts, pages, and archives.', 'apus-theme') . '</p>';
}
function apus_performance_section_callback() {
echo '<p>' . __('Optimize your site performance with these settings.', 'apus-theme') . '</p>';
}
function apus_related_posts_section_callback() {
echo '<p>' . __('Configure related posts display on single post pages.', 'apus-theme') . '</p>';
}
function apus_social_share_section_callback() {
echo '<p>' . __('Configure social share buttons display on single post pages.', 'apus-theme') . '</p>';
}
/**
* Sanitize all options
*
* @param array $input The input array
* @return array The sanitized array
*/
function apus_sanitize_options($input) {
$sanitized = array();
if (!is_array($input)) {
return $sanitized;
}
// General Settings
$sanitized['site_logo'] = isset($input['site_logo']) ? absint($input['site_logo']) : 0;
$sanitized['site_favicon'] = isset($input['site_favicon']) ? absint($input['site_favicon']) : 0;
$sanitized['enable_breadcrumbs'] = isset($input['enable_breadcrumbs']) ? (bool) $input['enable_breadcrumbs'] : false;
$sanitized['breadcrumb_separator'] = isset($input['breadcrumb_separator']) ? sanitize_text_field($input['breadcrumb_separator']) : '>';
$sanitized['date_format'] = isset($input['date_format']) ? sanitize_text_field($input['date_format']) : 'd/m/Y';
$sanitized['time_format'] = isset($input['time_format']) ? sanitize_text_field($input['time_format']) : 'H:i';
$sanitized['copyright_text'] = isset($input['copyright_text']) ? wp_kses_post($input['copyright_text']) : '';
// Social Media
$social_fields = array('facebook', 'twitter', 'instagram', 'linkedin', 'youtube');
foreach ($social_fields as $social) {
$key = 'social_' . $social;
$sanitized[$key] = isset($input[$key]) ? esc_url_raw($input[$key]) : '';
}
// Content Settings
$sanitized['excerpt_length'] = isset($input['excerpt_length']) ? absint($input['excerpt_length']) : 55;
$sanitized['excerpt_more'] = isset($input['excerpt_more']) ? sanitize_text_field($input['excerpt_more']) : '...';
$sanitized['default_post_layout'] = isset($input['default_post_layout']) ? sanitize_text_field($input['default_post_layout']) : 'right-sidebar';
$sanitized['default_page_layout'] = isset($input['default_page_layout']) ? sanitize_text_field($input['default_page_layout']) : 'right-sidebar';
$sanitized['archive_posts_per_page'] = isset($input['archive_posts_per_page']) ? absint($input['archive_posts_per_page']) : 10;
$sanitized['show_featured_image_single'] = isset($input['show_featured_image_single']) ? (bool) $input['show_featured_image_single'] : false;
$sanitized['show_author_box'] = isset($input['show_author_box']) ? (bool) $input['show_author_box'] : false;
$sanitized['enable_comments_posts'] = isset($input['enable_comments_posts']) ? (bool) $input['enable_comments_posts'] : false;
$sanitized['enable_comments_pages'] = isset($input['enable_comments_pages']) ? (bool) $input['enable_comments_pages'] : false;
$sanitized['show_post_meta'] = isset($input['show_post_meta']) ? (bool) $input['show_post_meta'] : false;
$sanitized['show_post_tags'] = isset($input['show_post_tags']) ? (bool) $input['show_post_tags'] : false;
$sanitized['show_post_categories'] = isset($input['show_post_categories']) ? (bool) $input['show_post_categories'] : false;
// Performance Settings
$sanitized['enable_lazy_loading'] = isset($input['enable_lazy_loading']) ? (bool) $input['enable_lazy_loading'] : false;
$sanitized['performance_remove_emoji'] = isset($input['performance_remove_emoji']) ? (bool) $input['performance_remove_emoji'] : false;
$sanitized['performance_remove_embeds'] = isset($input['performance_remove_embeds']) ? (bool) $input['performance_remove_embeds'] : false;
$sanitized['performance_remove_dashicons'] = isset($input['performance_remove_dashicons']) ? (bool) $input['performance_remove_dashicons'] : false;
$sanitized['performance_defer_js'] = isset($input['performance_defer_js']) ? (bool) $input['performance_defer_js'] : false;
$sanitized['performance_minify_html'] = isset($input['performance_minify_html']) ? (bool) $input['performance_minify_html'] : false;
$sanitized['performance_disable_gutenberg'] = isset($input['performance_disable_gutenberg']) ? (bool) $input['performance_disable_gutenberg'] : false;
// Related Posts
$sanitized['enable_related_posts'] = isset($input['enable_related_posts']) ? (bool) $input['enable_related_posts'] : false;
$sanitized['related_posts_count'] = isset($input['related_posts_count']) ? absint($input['related_posts_count']) : 3;
$sanitized['related_posts_taxonomy'] = isset($input['related_posts_taxonomy']) ? sanitize_text_field($input['related_posts_taxonomy']) : 'category';
$sanitized['related_posts_title'] = isset($input['related_posts_title']) ? sanitize_text_field($input['related_posts_title']) : __('Related Posts', 'apus-theme');
$sanitized['related_posts_columns'] = isset($input['related_posts_columns']) ? absint($input['related_posts_columns']) : 3;
// Social Share Buttons
$sanitized['apus_enable_share_buttons'] = isset($input['apus_enable_share_buttons']) ? sanitize_text_field($input['apus_enable_share_buttons']) : '1';
$sanitized['apus_share_text'] = isset($input['apus_share_text']) ? sanitize_text_field($input['apus_share_text']) : __('Compartir:', 'apus-theme');
// Advanced Settings
$sanitized['custom_css'] = isset($input['custom_css']) ? apus_sanitize_css($input['custom_css']) : '';
$sanitized['custom_js_header'] = isset($input['custom_js_header']) ? apus_sanitize_js($input['custom_js_header']) : '';
$sanitized['custom_js_footer'] = isset($input['custom_js_footer']) ? apus_sanitize_js($input['custom_js_footer']) : '';
return $sanitized;
}
/**
* NOTE: All sanitization functions have been moved to inc/sanitize-functions.php
* to avoid function redeclaration errors. This includes:
* - apus_sanitize_css()
* - apus_sanitize_js()
* - apus_sanitize_integer()
* - apus_sanitize_text()
* - apus_sanitize_url()
* - apus_sanitize_html()
* - apus_sanitize_checkbox()
* - apus_sanitize_select()
*/

View File

@@ -0,0 +1,661 @@
<?php
/**
* Theme Options Page Template
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// Get current options
$options = get_option('apus_theme_options', apus_get_default_options());
?>
<div class="wrap apus-theme-options">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<div class="apus-options-header">
<div class="apus-options-logo">
<h2><?php _e('Apus Theme', 'apus-theme'); ?></h2>
<span class="version"><?php echo 'v' . APUS_VERSION; ?></span>
</div>
<div class="apus-options-actions">
<button type="button" class="button button-secondary" id="apus-export-options">
<span class="dashicons dashicons-download"></span>
<?php _e('Export Options', 'apus-theme'); ?>
</button>
<button type="button" class="button button-secondary" id="apus-import-options">
<span class="dashicons dashicons-upload"></span>
<?php _e('Import Options', 'apus-theme'); ?>
</button>
<button type="button" class="button button-secondary" id="apus-reset-options">
<span class="dashicons dashicons-image-rotate"></span>
<?php _e('Reset to Defaults', 'apus-theme'); ?>
</button>
</div>
</div>
<form method="post" action="options.php" class="apus-options-form">
<?php
settings_fields('apus_theme_options_group');
?>
<div class="apus-options-container">
<!-- Tabs Navigation -->
<div class="apus-tabs-nav">
<ul>
<li class="active">
<a href="#general" data-tab="general">
<span class="dashicons dashicons-admin-settings"></span>
<?php _e('General', 'apus-theme'); ?>
</a>
</li>
<li>
<a href="#content" data-tab="content">
<span class="dashicons dashicons-edit-page"></span>
<?php _e('Content', 'apus-theme'); ?>
</a>
</li>
<li>
<a href="#performance" data-tab="performance">
<span class="dashicons dashicons-performance"></span>
<?php _e('Performance', 'apus-theme'); ?>
</a>
</li>
<li>
<a href="#related-posts" data-tab="related-posts">
<span class="dashicons dashicons-admin-links"></span>
<?php _e('Related Posts', 'apus-theme'); ?>
</a>
</li>
<li>
<a href="#advanced" data-tab="advanced">
<span class="dashicons dashicons-admin-tools"></span>
<?php _e('Advanced', 'apus-theme'); ?>
</a>
</li>
</ul>
</div>
<!-- Tabs Content -->
<div class="apus-tabs-content">
<!-- General Tab -->
<div id="general" class="apus-tab-pane active">
<h2><?php _e('General Settings', 'apus-theme'); ?></h2>
<p class="description"><?php _e('Configure general theme settings including logo, branding, and social media.', 'apus-theme'); ?></p>
<table class="form-table">
<!-- Site Logo -->
<tr>
<th scope="row">
<label for="site_logo"><?php _e('Site Logo', 'apus-theme'); ?></label>
</th>
<td>
<div class="apus-image-upload">
<input type="hidden" name="apus_theme_options[site_logo]" id="site_logo" value="<?php echo esc_attr($options['site_logo'] ?? 0); ?>" class="apus-image-id" />
<div class="apus-image-preview">
<?php
$logo_id = $options['site_logo'] ?? 0;
if ($logo_id) {
echo wp_get_attachment_image($logo_id, 'medium', false, array('class' => 'apus-preview-image'));
}
?>
</div>
<button type="button" class="button apus-upload-image"><?php _e('Upload Logo', 'apus-theme'); ?></button>
<button type="button" class="button apus-remove-image" <?php echo (!$logo_id ? 'style="display:none;"' : ''); ?>><?php _e('Remove Logo', 'apus-theme'); ?></button>
<p class="description"><?php _e('Upload your site logo. Recommended size: 200x60px', 'apus-theme'); ?></p>
</div>
</td>
</tr>
<!-- Site Favicon -->
<tr>
<th scope="row">
<label for="site_favicon"><?php _e('Site Favicon', 'apus-theme'); ?></label>
</th>
<td>
<div class="apus-image-upload">
<input type="hidden" name="apus_theme_options[site_favicon]" id="site_favicon" value="<?php echo esc_attr($options['site_favicon'] ?? 0); ?>" class="apus-image-id" />
<div class="apus-image-preview">
<?php
$favicon_id = $options['site_favicon'] ?? 0;
if ($favicon_id) {
echo wp_get_attachment_image($favicon_id, 'thumbnail', false, array('class' => 'apus-preview-image'));
}
?>
</div>
<button type="button" class="button apus-upload-image"><?php _e('Upload Favicon', 'apus-theme'); ?></button>
<button type="button" class="button apus-remove-image" <?php echo (!$favicon_id ? 'style="display:none;"' : ''); ?>><?php _e('Remove Favicon', 'apus-theme'); ?></button>
<p class="description"><?php _e('Upload your site favicon. Recommended size: 32x32px or 64x64px', 'apus-theme'); ?></p>
</div>
</td>
</tr>
<!-- Enable Breadcrumbs -->
<tr>
<th scope="row">
<label for="enable_breadcrumbs"><?php _e('Enable Breadcrumbs', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[enable_breadcrumbs]" id="enable_breadcrumbs" value="1" <?php checked(isset($options['enable_breadcrumbs']) ? $options['enable_breadcrumbs'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Show breadcrumbs navigation on pages and posts', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Breadcrumb Separator -->
<tr>
<th scope="row">
<label for="breadcrumb_separator"><?php _e('Breadcrumb Separator', 'apus-theme'); ?></label>
</th>
<td>
<input type="text" name="apus_theme_options[breadcrumb_separator]" id="breadcrumb_separator" value="<?php echo esc_attr($options['breadcrumb_separator'] ?? '>'); ?>" class="regular-text" />
<p class="description"><?php _e('Character or symbol to separate breadcrumb items (e.g., >, /, »)', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Date Format -->
<tr>
<th scope="row">
<label for="date_format"><?php _e('Date Format', 'apus-theme'); ?></label>
</th>
<td>
<input type="text" name="apus_theme_options[date_format]" id="date_format" value="<?php echo esc_attr($options['date_format'] ?? 'd/m/Y'); ?>" class="regular-text" />
<p class="description"><?php _e('PHP date format (e.g., d/m/Y, m/d/Y, Y-m-d)', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Time Format -->
<tr>
<th scope="row">
<label for="time_format"><?php _e('Time Format', 'apus-theme'); ?></label>
</th>
<td>
<input type="text" name="apus_theme_options[time_format]" id="time_format" value="<?php echo esc_attr($options['time_format'] ?? 'H:i'); ?>" class="regular-text" />
<p class="description"><?php _e('PHP time format (e.g., H:i, g:i A)', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Copyright Text -->
<tr>
<th scope="row">
<label for="copyright_text"><?php _e('Copyright Text', 'apus-theme'); ?></label>
</th>
<td>
<textarea name="apus_theme_options[copyright_text]" id="copyright_text" rows="3" class="large-text"><?php echo esc_textarea($options['copyright_text'] ?? sprintf(__('&copy; %s %s. All rights reserved.', 'apus-theme'), date('Y'), get_bloginfo('name'))); ?></textarea>
<p class="description"><?php _e('Footer copyright text. HTML allowed.', 'apus-theme'); ?></p>
</td>
</tr>
</table>
<h3><?php _e('Social Media Links', 'apus-theme'); ?></h3>
<table class="form-table">
<!-- Facebook -->
<tr>
<th scope="row">
<label for="social_facebook"><?php _e('Facebook URL', 'apus-theme'); ?></label>
</th>
<td>
<input type="url" name="apus_theme_options[social_facebook]" id="social_facebook" value="<?php echo esc_url($options['social_facebook'] ?? ''); ?>" class="regular-text" placeholder="https://facebook.com/yourpage" />
</td>
</tr>
<!-- Twitter -->
<tr>
<th scope="row">
<label for="social_twitter"><?php _e('Twitter URL', 'apus-theme'); ?></label>
</th>
<td>
<input type="url" name="apus_theme_options[social_twitter]" id="social_twitter" value="<?php echo esc_url($options['social_twitter'] ?? ''); ?>" class="regular-text" placeholder="https://twitter.com/youraccount" />
</td>
</tr>
<!-- Instagram -->
<tr>
<th scope="row">
<label for="social_instagram"><?php _e('Instagram URL', 'apus-theme'); ?></label>
</th>
<td>
<input type="url" name="apus_theme_options[social_instagram]" id="social_instagram" value="<?php echo esc_url($options['social_instagram'] ?? ''); ?>" class="regular-text" placeholder="https://instagram.com/youraccount" />
</td>
</tr>
<!-- LinkedIn -->
<tr>
<th scope="row">
<label for="social_linkedin"><?php _e('LinkedIn URL', 'apus-theme'); ?></label>
</th>
<td>
<input type="url" name="apus_theme_options[social_linkedin]" id="social_linkedin" value="<?php echo esc_url($options['social_linkedin'] ?? ''); ?>" class="regular-text" placeholder="https://linkedin.com/company/yourcompany" />
</td>
</tr>
<!-- YouTube -->
<tr>
<th scope="row">
<label for="social_youtube"><?php _e('YouTube URL', 'apus-theme'); ?></label>
</th>
<td>
<input type="url" name="apus_theme_options[social_youtube]" id="social_youtube" value="<?php echo esc_url($options['social_youtube'] ?? ''); ?>" class="regular-text" placeholder="https://youtube.com/yourchannel" />
</td>
</tr>
</table>
</div>
<!-- Content Tab -->
<div id="content" class="apus-tab-pane">
<h2><?php _e('Content Settings', 'apus-theme'); ?></h2>
<p class="description"><?php _e('Configure content display settings for posts, pages, and archives.', 'apus-theme'); ?></p>
<table class="form-table">
<!-- Excerpt Length -->
<tr>
<th scope="row">
<label for="excerpt_length"><?php _e('Excerpt Length', 'apus-theme'); ?></label>
</th>
<td>
<input type="number" name="apus_theme_options[excerpt_length]" id="excerpt_length" value="<?php echo esc_attr($options['excerpt_length'] ?? 55); ?>" class="small-text" min="10" max="500" />
<p class="description"><?php _e('Number of words to show in excerpt', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Excerpt More -->
<tr>
<th scope="row">
<label for="excerpt_more"><?php _e('Excerpt More Text', 'apus-theme'); ?></label>
</th>
<td>
<input type="text" name="apus_theme_options[excerpt_more]" id="excerpt_more" value="<?php echo esc_attr($options['excerpt_more'] ?? '...'); ?>" class="regular-text" />
<p class="description"><?php _e('Text to append at the end of excerpts', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Default Post Layout -->
<tr>
<th scope="row">
<label for="default_post_layout"><?php _e('Default Post Layout', 'apus-theme'); ?></label>
</th>
<td>
<select name="apus_theme_options[default_post_layout]" id="default_post_layout">
<option value="right-sidebar" <?php selected($options['default_post_layout'] ?? 'right-sidebar', 'right-sidebar'); ?>><?php _e('Right Sidebar', 'apus-theme'); ?></option>
<option value="left-sidebar" <?php selected($options['default_post_layout'] ?? 'right-sidebar', 'left-sidebar'); ?>><?php _e('Left Sidebar', 'apus-theme'); ?></option>
<option value="no-sidebar" <?php selected($options['default_post_layout'] ?? 'right-sidebar', 'no-sidebar'); ?>><?php _e('No Sidebar (Full Width)', 'apus-theme'); ?></option>
</select>
<p class="description"><?php _e('Default layout for single posts', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Default Page Layout -->
<tr>
<th scope="row">
<label for="default_page_layout"><?php _e('Default Page Layout', 'apus-theme'); ?></label>
</th>
<td>
<select name="apus_theme_options[default_page_layout]" id="default_page_layout">
<option value="right-sidebar" <?php selected($options['default_page_layout'] ?? 'right-sidebar', 'right-sidebar'); ?>><?php _e('Right Sidebar', 'apus-theme'); ?></option>
<option value="left-sidebar" <?php selected($options['default_page_layout'] ?? 'right-sidebar', 'left-sidebar'); ?>><?php _e('Left Sidebar', 'apus-theme'); ?></option>
<option value="no-sidebar" <?php selected($options['default_page_layout'] ?? 'right-sidebar', 'no-sidebar'); ?>><?php _e('No Sidebar (Full Width)', 'apus-theme'); ?></option>
</select>
<p class="description"><?php _e('Default layout for pages', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Archive Posts Per Page -->
<tr>
<th scope="row">
<label for="archive_posts_per_page"><?php _e('Archive Posts Per Page', 'apus-theme'); ?></label>
</th>
<td>
<input type="number" name="apus_theme_options[archive_posts_per_page]" id="archive_posts_per_page" value="<?php echo esc_attr($options['archive_posts_per_page'] ?? 10); ?>" class="small-text" min="1" max="100" />
<p class="description"><?php _e('Number of posts to show on archive pages. Set to 0 to use WordPress default.', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Show Featured Image on Single Posts -->
<tr>
<th scope="row">
<label for="show_featured_image_single"><?php _e('Show Featured Image', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[show_featured_image_single]" id="show_featured_image_single" value="1" <?php checked(isset($options['show_featured_image_single']) ? $options['show_featured_image_single'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Display featured image at the top of single posts', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Show Author Box -->
<tr>
<th scope="row">
<label for="show_author_box"><?php _e('Show Author Box', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[show_author_box]" id="show_author_box" value="1" <?php checked(isset($options['show_author_box']) ? $options['show_author_box'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Display author information box on single posts', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Enable Comments on Posts -->
<tr>
<th scope="row">
<label for="enable_comments_posts"><?php _e('Enable Comments on Posts', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[enable_comments_posts]" id="enable_comments_posts" value="1" <?php checked(isset($options['enable_comments_posts']) ? $options['enable_comments_posts'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Allow comments on blog posts', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Enable Comments on Pages -->
<tr>
<th scope="row">
<label for="enable_comments_pages"><?php _e('Enable Comments on Pages', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[enable_comments_pages]" id="enable_comments_pages" value="1" <?php checked(isset($options['enable_comments_pages']) ? $options['enable_comments_pages'] : false, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Allow comments on pages', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Show Post Meta -->
<tr>
<th scope="row">
<label for="show_post_meta"><?php _e('Show Post Meta', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[show_post_meta]" id="show_post_meta" value="1" <?php checked(isset($options['show_post_meta']) ? $options['show_post_meta'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Display post meta information (date, author, etc.)', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Show Post Tags -->
<tr>
<th scope="row">
<label for="show_post_tags"><?php _e('Show Post Tags', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[show_post_tags]" id="show_post_tags" value="1" <?php checked(isset($options['show_post_tags']) ? $options['show_post_tags'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Display tags on single posts', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Show Post Categories -->
<tr>
<th scope="row">
<label for="show_post_categories"><?php _e('Show Post Categories', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[show_post_categories]" id="show_post_categories" value="1" <?php checked(isset($options['show_post_categories']) ? $options['show_post_categories'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Display categories on single posts', 'apus-theme'); ?></p>
</td>
</tr>
</table>
</div>
<!-- Performance Tab -->
<div id="performance" class="apus-tab-pane">
<h2><?php _e('Performance Settings', 'apus-theme'); ?></h2>
<p class="description"><?php _e('Optimize your site performance with these settings. Be careful when enabling these options.', 'apus-theme'); ?></p>
<table class="form-table">
<!-- Enable Lazy Loading -->
<tr>
<th scope="row">
<label for="enable_lazy_loading"><?php _e('Enable Lazy Loading', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[enable_lazy_loading]" id="enable_lazy_loading" value="1" <?php checked(isset($options['enable_lazy_loading']) ? $options['enable_lazy_loading'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Enable lazy loading for images to improve page load times', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Remove Emoji Scripts -->
<tr>
<th scope="row">
<label for="performance_remove_emoji"><?php _e('Remove Emoji Scripts', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[performance_remove_emoji]" id="performance_remove_emoji" value="1" <?php checked(isset($options['performance_remove_emoji']) ? $options['performance_remove_emoji'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Remove WordPress emoji scripts and styles (reduces HTTP requests)', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Remove Embeds -->
<tr>
<th scope="row">
<label for="performance_remove_embeds"><?php _e('Remove Embeds', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[performance_remove_embeds]" id="performance_remove_embeds" value="1" <?php checked(isset($options['performance_remove_embeds']) ? $options['performance_remove_embeds'] : false, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Remove WordPress embed scripts if you don\'t use oEmbed', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Remove Dashicons on Frontend -->
<tr>
<th scope="row">
<label for="performance_remove_dashicons"><?php _e('Remove Dashicons', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[performance_remove_dashicons]" id="performance_remove_dashicons" value="1" <?php checked(isset($options['performance_remove_dashicons']) ? $options['performance_remove_dashicons'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Remove Dashicons from frontend for non-logged in users', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Defer JavaScript -->
<tr>
<th scope="row">
<label for="performance_defer_js"><?php _e('Defer JavaScript', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[performance_defer_js]" id="performance_defer_js" value="1" <?php checked(isset($options['performance_defer_js']) ? $options['performance_defer_js'] : false, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Add defer attribute to JavaScript files (may break some scripts)', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Minify HTML -->
<tr>
<th scope="row">
<label for="performance_minify_html"><?php _e('Minify HTML', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[performance_minify_html]" id="performance_minify_html" value="1" <?php checked(isset($options['performance_minify_html']) ? $options['performance_minify_html'] : false, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Minify HTML output to reduce page size', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Disable Gutenberg -->
<tr>
<th scope="row">
<label for="performance_disable_gutenberg"><?php _e('Disable Gutenberg', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[performance_disable_gutenberg]" id="performance_disable_gutenberg" value="1" <?php checked(isset($options['performance_disable_gutenberg']) ? $options['performance_disable_gutenberg'] : false, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Disable Gutenberg editor and revert to classic editor', 'apus-theme'); ?></p>
</td>
</tr>
</table>
</div>
<!-- Related Posts Tab -->
<div id="related-posts" class="apus-tab-pane">
<h2><?php _e('Related Posts Settings', 'apus-theme'); ?></h2>
<p class="description"><?php _e('Configure related posts display on single post pages.', 'apus-theme'); ?></p>
<table class="form-table">
<!-- Enable Related Posts -->
<tr>
<th scope="row">
<label for="enable_related_posts"><?php _e('Enable Related Posts', 'apus-theme'); ?></label>
</th>
<td>
<label class="apus-switch">
<input type="checkbox" name="apus_theme_options[enable_related_posts]" id="enable_related_posts" value="1" <?php checked(isset($options['enable_related_posts']) ? $options['enable_related_posts'] : true, true); ?> />
<span class="apus-slider"></span>
</label>
<p class="description"><?php _e('Show related posts at the end of single posts', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Related Posts Count -->
<tr>
<th scope="row">
<label for="related_posts_count"><?php _e('Number of Related Posts', 'apus-theme'); ?></label>
</th>
<td>
<input type="number" name="apus_theme_options[related_posts_count]" id="related_posts_count" value="<?php echo esc_attr($options['related_posts_count'] ?? 3); ?>" class="small-text" min="1" max="12" />
<p class="description"><?php _e('How many related posts to display', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Related Posts Taxonomy -->
<tr>
<th scope="row">
<label for="related_posts_taxonomy"><?php _e('Relate Posts By', 'apus-theme'); ?></label>
</th>
<td>
<select name="apus_theme_options[related_posts_taxonomy]" id="related_posts_taxonomy">
<option value="category" <?php selected($options['related_posts_taxonomy'] ?? 'category', 'category'); ?>><?php _e('Category', 'apus-theme'); ?></option>
<option value="tag" <?php selected($options['related_posts_taxonomy'] ?? 'category', 'tag'); ?>><?php _e('Tag', 'apus-theme'); ?></option>
<option value="both" <?php selected($options['related_posts_taxonomy'] ?? 'category', 'both'); ?>><?php _e('Category and Tag', 'apus-theme'); ?></option>
</select>
<p class="description"><?php _e('How to determine related posts', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Related Posts Title -->
<tr>
<th scope="row">
<label for="related_posts_title"><?php _e('Related Posts Title', 'apus-theme'); ?></label>
</th>
<td>
<input type="text" name="apus_theme_options[related_posts_title]" id="related_posts_title" value="<?php echo esc_attr($options['related_posts_title'] ?? __('Related Posts', 'apus-theme')); ?>" class="regular-text" />
<p class="description"><?php _e('Title to display above related posts section', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Related Posts Columns -->
<tr>
<th scope="row">
<label for="related_posts_columns"><?php _e('Columns', 'apus-theme'); ?></label>
</th>
<td>
<select name="apus_theme_options[related_posts_columns]" id="related_posts_columns">
<option value="2" <?php selected($options['related_posts_columns'] ?? 3, 2); ?>><?php _e('2 Columns', 'apus-theme'); ?></option>
<option value="3" <?php selected($options['related_posts_columns'] ?? 3, 3); ?>><?php _e('3 Columns', 'apus-theme'); ?></option>
<option value="4" <?php selected($options['related_posts_columns'] ?? 3, 4); ?>><?php _e('4 Columns', 'apus-theme'); ?></option>
</select>
<p class="description"><?php _e('Number of columns to display related posts', 'apus-theme'); ?></p>
</td>
</tr>
</table>
</div>
<!-- Advanced Tab -->
<div id="advanced" class="apus-tab-pane">
<h2><?php _e('Advanced Settings', 'apus-theme'); ?></h2>
<p class="description"><?php _e('Advanced customization options. Use with caution.', 'apus-theme'); ?></p>
<table class="form-table">
<!-- Custom CSS -->
<tr>
<th scope="row">
<label for="custom_css"><?php _e('Custom CSS', 'apus-theme'); ?></label>
</th>
<td>
<textarea name="apus_theme_options[custom_css]" id="custom_css" rows="10" class="large-text code"><?php echo esc_textarea($options['custom_css'] ?? ''); ?></textarea>
<p class="description"><?php _e('Add custom CSS code. This will be added to the &lt;head&gt; section.', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Custom JS Header -->
<tr>
<th scope="row">
<label for="custom_js_header"><?php _e('Custom JavaScript (Header)', 'apus-theme'); ?></label>
</th>
<td>
<textarea name="apus_theme_options[custom_js_header]" id="custom_js_header" rows="10" class="large-text code"><?php echo esc_textarea($options['custom_js_header'] ?? ''); ?></textarea>
<p class="description"><?php _e('Add custom JavaScript code. This will be added to the &lt;head&gt; section. Do not include &lt;script&gt; tags.', 'apus-theme'); ?></p>
</td>
</tr>
<!-- Custom JS Footer -->
<tr>
<th scope="row">
<label for="custom_js_footer"><?php _e('Custom JavaScript (Footer)', 'apus-theme'); ?></label>
</th>
<td>
<textarea name="apus_theme_options[custom_js_footer]" id="custom_js_footer" rows="10" class="large-text code"><?php echo esc_textarea($options['custom_js_footer'] ?? ''); ?></textarea>
<p class="description"><?php _e('Add custom JavaScript code. This will be added before the closing &lt;/body&gt; tag. Do not include &lt;script&gt; tags.', 'apus-theme'); ?></p>
</td>
</tr>
</table>
</div>
</div>
</div>
<?php submit_button(__('Save All Settings', 'apus-theme'), 'primary large', 'submit', true); ?>
</form>
</div>
<!-- Import Modal -->
<div id="apus-import-modal" class="apus-modal" style="display:none;">
<div class="apus-modal-content">
<span class="apus-modal-close">&times;</span>
<h2><?php _e('Import Options', 'apus-theme'); ?></h2>
<p><?php _e('Paste your exported options JSON here:', 'apus-theme'); ?></p>
<textarea id="apus-import-data" rows="10" class="large-text code"></textarea>
<p>
<button type="button" class="button button-primary" id="apus-import-submit"><?php _e('Import', 'apus-theme'); ?></button>
<button type="button" class="button" id="apus-import-cancel"><?php _e('Cancel', 'apus-theme'); ?></button>
</p>
</div>
</div>

View File

@@ -0,0 +1,272 @@
<?php
/**
* Related Posts Configuration Options
*
* This file provides helper functions and documentation for configuring
* related posts functionality via WordPress options.
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Get all related posts options with their current values
*
* @return array Array of options with their values
*/
function apus_get_related_posts_options() {
return array(
'enabled' => array(
'key' => 'apus_related_posts_enabled',
'value' => get_option('apus_related_posts_enabled', true),
'type' => 'boolean',
'default' => true,
'label' => __('Enable Related Posts', 'apus-theme'),
'description' => __('Show related posts section at the end of single posts', 'apus-theme'),
),
'title' => array(
'key' => 'apus_related_posts_title',
'value' => get_option('apus_related_posts_title', __('Related Posts', 'apus-theme')),
'type' => 'text',
'default' => __('Related Posts', 'apus-theme'),
'label' => __('Section Title', 'apus-theme'),
'description' => __('Title displayed above related posts', 'apus-theme'),
),
'count' => array(
'key' => 'apus_related_posts_count',
'value' => get_option('apus_related_posts_count', 3),
'type' => 'number',
'default' => 3,
'min' => 1,
'max' => 12,
'label' => __('Number of Posts', 'apus-theme'),
'description' => __('Maximum number of related posts to display', 'apus-theme'),
),
'columns' => array(
'key' => 'apus_related_posts_columns',
'value' => get_option('apus_related_posts_columns', 3),
'type' => 'select',
'default' => 3,
'options' => array(
1 => __('1 Column', 'apus-theme'),
2 => __('2 Columns', 'apus-theme'),
3 => __('3 Columns', 'apus-theme'),
4 => __('4 Columns', 'apus-theme'),
),
'label' => __('Grid Columns', 'apus-theme'),
'description' => __('Number of columns in the grid layout (responsive)', 'apus-theme'),
),
'show_excerpt' => array(
'key' => 'apus_related_posts_show_excerpt',
'value' => get_option('apus_related_posts_show_excerpt', true),
'type' => 'boolean',
'default' => true,
'label' => __('Show Excerpt', 'apus-theme'),
'description' => __('Display post excerpt in related posts cards', 'apus-theme'),
),
'excerpt_length' => array(
'key' => 'apus_related_posts_excerpt_length',
'value' => get_option('apus_related_posts_excerpt_length', 20),
'type' => 'number',
'default' => 20,
'min' => 5,
'max' => 100,
'label' => __('Excerpt Length', 'apus-theme'),
'description' => __('Number of words in the excerpt', 'apus-theme'),
),
'show_date' => array(
'key' => 'apus_related_posts_show_date',
'value' => get_option('apus_related_posts_show_date', true),
'type' => 'boolean',
'default' => true,
'label' => __('Show Date', 'apus-theme'),
'description' => __('Display publication date in related posts', 'apus-theme'),
),
'show_category' => array(
'key' => 'apus_related_posts_show_category',
'value' => get_option('apus_related_posts_show_category', true),
'type' => 'boolean',
'default' => true,
'label' => __('Show Category', 'apus-theme'),
'description' => __('Display category badge on related posts', 'apus-theme'),
),
'bg_colors' => array(
'key' => 'apus_related_posts_bg_colors',
'value' => get_option('apus_related_posts_bg_colors', array(
'#1a73e8', '#e91e63', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4',
)),
'type' => 'color_array',
'default' => array(
'#1a73e8', // Blue
'#e91e63', // Pink
'#4caf50', // Green
'#ff9800', // Orange
'#9c27b0', // Purple
'#00bcd4', // Cyan
),
'label' => __('Background Colors', 'apus-theme'),
'description' => __('Colors used for posts without featured images', 'apus-theme'),
),
);
}
/**
* Update a related posts option
*
* @param string $option_key The option key (without 'apus_related_posts_' prefix)
* @param mixed $value The new value
* @return bool True if updated successfully
*/
function apus_update_related_posts_option($option_key, $value) {
$full_key = 'apus_related_posts_' . $option_key;
return update_option($full_key, $value);
}
/**
* Reset related posts options to defaults
*
* @return bool True if reset successfully
*/
function apus_reset_related_posts_options() {
$options = apus_get_related_posts_options();
$success = true;
foreach ($options as $option) {
if (!update_option($option['key'], $option['default'])) {
$success = false;
}
}
return $success;
}
/**
* Example: Programmatically configure related posts
*
* This function shows how to configure related posts options programmatically.
* You can call this from your functions.php or a plugin.
*
* @return void
*/
function apus_example_configure_related_posts() {
// Example usage - uncomment to use:
// Enable related posts
// update_option('apus_related_posts_enabled', true);
// Set custom title
// update_option('apus_related_posts_title', __('You Might Also Like', 'apus-theme'));
// Show 4 related posts
// update_option('apus_related_posts_count', 4);
// Use 2 columns layout
// update_option('apus_related_posts_columns', 2);
// Show excerpt with 30 words
// update_option('apus_related_posts_show_excerpt', true);
// update_option('apus_related_posts_excerpt_length', 30);
// Show date and category
// update_option('apus_related_posts_show_date', true);
// update_option('apus_related_posts_show_category', true);
// Custom background colors for posts without images
// update_option('apus_related_posts_bg_colors', array(
// '#FF6B6B', // Red
// '#4ECDC4', // Teal
// '#45B7D1', // Blue
// '#FFA07A', // Coral
// '#98D8C8', // Mint
// '#F7DC6F', // Yellow
// ));
}
/**
* Filter hook example: Modify related posts query
*
* This example shows how to customize the related posts query.
* Add this to your functions.php or child theme.
*/
function apus_example_modify_related_posts_query($args, $post_id) {
// Example: Order by date instead of random
// $args['orderby'] = 'date';
// $args['order'] = 'DESC';
// Example: Only show posts from the last 6 months
// $args['date_query'] = array(
// array(
// 'after' => '6 months ago',
// ),
// );
// Example: Exclude specific category
// $args['category__not_in'] = array(5); // Replace 5 with category ID
return $args;
}
// add_filter('apus_related_posts_args', 'apus_example_modify_related_posts_query', 10, 2);
/**
* Get documentation for related posts configuration
*
* @return array Documentation array
*/
function apus_get_related_posts_documentation() {
return array(
'overview' => array(
'title' => __('Related Posts Overview', 'apus-theme'),
'content' => __(
'The related posts feature automatically displays relevant posts at the end of each blog post. ' .
'Posts are related based on shared categories and displayed in a responsive Bootstrap grid.',
'apus-theme'
),
),
'features' => array(
'title' => __('Key Features', 'apus-theme'),
'items' => array(
__('Automatic category-based matching', 'apus-theme'),
__('Responsive Bootstrap 5 grid layout', 'apus-theme'),
__('Configurable number of posts and columns', 'apus-theme'),
__('Support for posts with and without featured images', 'apus-theme'),
__('Beautiful color backgrounds for posts without images', 'apus-theme'),
__('Customizable excerpt length', 'apus-theme'),
__('Optional display of dates and categories', 'apus-theme'),
__('Smooth hover animations', 'apus-theme'),
__('Print-friendly styles', 'apus-theme'),
__('Dark mode support', 'apus-theme'),
),
),
'configuration' => array(
'title' => __('How to Configure', 'apus-theme'),
'methods' => array(
'database' => array(
'title' => __('Via WordPress Options API', 'apus-theme'),
'code' => "update_option('apus_related_posts_enabled', true);\nupdate_option('apus_related_posts_count', 4);",
),
'filter' => array(
'title' => __('Via Filter Hook', 'apus-theme'),
'code' => "add_filter('apus_related_posts_args', function(\$args, \$post_id) {\n \$args['posts_per_page'] = 6;\n return \$args;\n}, 10, 2);",
),
),
),
'customization' => array(
'title' => __('Customization Examples', 'apus-theme'),
'examples' => array(
array(
'title' => __('Change title and layout', 'apus-theme'),
'code' => "update_option('apus_related_posts_title', 'También te puede interesar');\nupdate_option('apus_related_posts_columns', 4);",
),
array(
'title' => __('Customize colors', 'apus-theme'),
'code' => "update_option('apus_related_posts_bg_colors', array(\n '#FF6B6B',\n '#4ECDC4',\n '#45B7D1'\n));",
),
),
),
);
}

214
inc/admin/theme-options.php Normal file
View File

@@ -0,0 +1,214 @@
<?php
/**
* Theme Options Admin Page
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Add admin menu
*/
function apus_add_admin_menu() {
add_theme_page(
__('Apus Theme Options', 'apus-theme'), // Page title
__('Theme Options', 'apus-theme'), // Menu title
'manage_options', // Capability
'apus-theme-options', // Menu slug
'apus_render_options_page', // Callback function
30 // Position
);
}
add_action('admin_menu', 'apus_add_admin_menu');
/**
* Render the options page
*/
function apus_render_options_page() {
// Check user capabilities
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'apus-theme'));
}
// Load the template
include get_template_directory() . '/inc/admin/options-page-template.php';
}
/**
* Enqueue admin scripts and styles
*/
function apus_enqueue_admin_scripts($hook) {
// Only load on our theme options page
if ($hook !== 'appearance_page_apus-theme-options') {
return;
}
// Enqueue WordPress media uploader
wp_enqueue_media();
// Enqueue admin styles
wp_enqueue_style(
'apus-admin-options',
get_template_directory_uri() . '/assets/admin/css/theme-options.css',
array(),
APUS_VERSION
);
// Enqueue admin scripts
wp_enqueue_script(
'apus-admin-options',
get_template_directory_uri() . '/assets/admin/js/theme-options.js',
array('jquery', 'wp-color-picker'),
APUS_VERSION,
true
);
// Localize script
wp_localize_script('apus-admin-options', 'apusAdminOptions', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('apus_admin_nonce'),
'strings' => array(
'selectImage' => __('Select Image', 'apus-theme'),
'useImage' => __('Use Image', 'apus-theme'),
'removeImage' => __('Remove Image', 'apus-theme'),
'confirmReset' => __('Are you sure you want to reset all options to default values? This cannot be undone.', 'apus-theme'),
'saved' => __('Settings saved successfully!', 'apus-theme'),
'error' => __('An error occurred while saving settings.', 'apus-theme'),
),
));
}
add_action('admin_enqueue_scripts', 'apus_enqueue_admin_scripts');
/**
* Add settings link to theme actions
*/
function apus_add_settings_link($links) {
$settings_link = '<a href="' . admin_url('themes.php?page=apus-theme-options') . '">' . __('Settings', 'apus-theme') . '</a>';
array_unshift($links, $settings_link);
return $links;
}
add_filter('theme_action_links_' . get_template(), 'apus_add_settings_link');
/**
* AJAX handler for resetting options
*/
function apus_reset_options_ajax() {
check_ajax_referer('apus_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Insufficient permissions.', 'apus-theme')));
}
// Delete options to reset to defaults
delete_option('apus_theme_options');
wp_send_json_success(array('message' => __('Options reset to defaults successfully.', 'apus-theme')));
}
add_action('wp_ajax_apus_reset_options', 'apus_reset_options_ajax');
/**
* AJAX handler for exporting options
*/
function apus_export_options_ajax() {
check_ajax_referer('apus_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Insufficient permissions.', 'apus-theme')));
}
$options = get_option('apus_theme_options', array());
wp_send_json_success(array(
'data' => json_encode($options, JSON_PRETTY_PRINT),
'filename' => 'apus-theme-options-' . date('Y-m-d') . '.json'
));
}
add_action('wp_ajax_apus_export_options', 'apus_export_options_ajax');
/**
* AJAX handler for importing options
*/
function apus_import_options_ajax() {
check_ajax_referer('apus_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(array('message' => __('Insufficient permissions.', 'apus-theme')));
}
if (!isset($_POST['import_data'])) {
wp_send_json_error(array('message' => __('No import data provided.', 'apus-theme')));
}
$import_data = json_decode(stripslashes($_POST['import_data']), true);
if (json_last_error() !== JSON_ERROR_NONE) {
wp_send_json_error(array('message' => __('Invalid JSON data.', 'apus-theme')));
}
// Sanitize imported data
$sanitized_data = apus_sanitize_options($import_data);
// Update options
update_option('apus_theme_options', $sanitized_data);
wp_send_json_success(array('message' => __('Options imported successfully.', 'apus-theme')));
}
add_action('wp_ajax_apus_import_options', 'apus_import_options_ajax');
/**
* Add admin notices
*/
function apus_admin_notices() {
$screen = get_current_screen();
if ($screen->id !== 'appearance_page_apus-theme-options') {
return;
}
// Check if settings were updated
if (isset($_GET['settings-updated']) && $_GET['settings-updated'] === 'true') {
?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Settings saved successfully!', 'apus-theme'); ?></p>
</div>
<?php
}
}
add_action('admin_notices', 'apus_admin_notices');
/**
* Register theme options in Customizer as well (for preview)
*/
function apus_customize_register($wp_customize) {
// Add a panel for theme options
$wp_customize->add_panel('apus_theme_options', array(
'title' => __('Apus Theme Options', 'apus-theme'),
'description' => __('Configure theme options (Also available in Theme Options page)', 'apus-theme'),
'priority' => 10,
));
// General Section
$wp_customize->add_section('apus_general', array(
'title' => __('General Settings', 'apus-theme'),
'panel' => 'apus_theme_options',
'priority' => 10,
));
// Enable breadcrumbs
$wp_customize->add_setting('apus_theme_options[enable_breadcrumbs]', array(
'default' => true,
'type' => 'option',
'sanitize_callback' => 'apus_sanitize_checkbox',
));
$wp_customize->add_control('apus_theme_options[enable_breadcrumbs]', array(
'label' => __('Enable Breadcrumbs', 'apus-theme'),
'section' => 'apus_general',
'type' => 'checkbox',
));
}
add_action('customize_register', 'apus_customize_register');

150
inc/adsense-delay.php Normal file
View File

@@ -0,0 +1,150 @@
<?php
/**
* AdSense Delay Loading Functionality
*
* Delays the loading of AdSense scripts until user interaction or timeout
* to improve initial page load performance.
*
* @package Apus_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 apus_delay_adsense_scripts() {
// Solo ejecutar en frontend
if (is_admin()) {
return;
}
// Verificar si el retardo de AdSense está habilitado en las opciones del tema
$delay_enabled = apus_get_option('apus_adsense_delay_enabled', '1');
if ($delay_enabled !== '1') {
return;
}
// Iniciar output buffering
ob_start('apus_replace_adsense_scripts');
}
add_action('template_redirect', 'apus_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 apus_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 Apus 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 apus_add_adsense_init_script() {
$delay_enabled = apus_get_option('apus_adsense_delay_enabled', '1');
if ($delay_enabled !== '1' || is_admin()) {
return;
}
?>
<script>
// Inicializar flag de retardo de AdSense
window.apusAdsenseDelayed = true;
</script>
<?php
}
add_action('wp_head', 'apus_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 > Apus Theme Options)
* 2. En la sección "Performance", activar la opción "Delay AdSense Loading"
* 3. Guardar cambios
*
* Comportamiento:
* - Los scripts de AdSense NO se cargarán hasta que el usuario:
* * Haga scroll en la página
* * Haga click en cualquier parte
* * Toque la pantalla (móviles)
* * Mueva el mouse
* * Presione una tecla
* - Si no hay interacción, los scripts se cargarán después de 5 segundos
*
* Beneficios:
* - Mejora significativa en Core Web Vitals (FID, TBT)
* - Reduce el tiempo de carga inicial de la página
* - No afecta la monetización (los ads se siguen mostrando)
* - Sin layout shifts al cargar los ads
*
* Para desactivar:
* - Desmarcar la opción en el panel de opciones del tema
* - Los scripts de AdSense se cargarán normalmente
*/

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

@@ -0,0 +1,316 @@
<?php
/**
* APU Tables Processing
* Funciones helper para tablas de Análisis de Precios Unitarios
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Procesa automáticamente tablas APU en el contenido
*
* Detecta tablas con el atributo data-apu y las envuelve
* con la clase .analisis para aplicar estilos específicos.
*
* @param string $content El contenido del post
* @return string El contenido procesado
*/
function apus_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', 'apus_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 apus_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', 'apus_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 apus_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', 'apus_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 apus_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 apus_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', 'apus_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 apus_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', 'apus_allow_apu_table_attributes', 10, 2);

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

@@ -0,0 +1,81 @@
<?php
/**
* Category Badge Functions
*
* Funciones para mostrar badge de categoría sobre el H1 en single posts.
* Utiliza clases de Bootstrap para el estilo del badge.
*
* @package Apus_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 apus_get_category_badge() {
// Verificar si la función está habilitada en las opciones del tema
$enabled = apus_get_option('show_category_badge', true);
if (!$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 apus_display_category_badge(); ?>
*
* @return void
*/
function apus_display_category_badge() {
echo apus_get_category_badge();
}

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

@@ -0,0 +1,253 @@
<?php
/**
* Desactivar completamente el sistema de comentarios
*
* Este archivo desactiva completamente los comentarios en WordPress,
* tanto en el frontend como en el área de administración.
*
* @package Apus_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 apus_disable_comments_status() {
return false;
}
add_filter('comments_open', 'apus_disable_comments_status', 20, 2);
add_filter('pings_open', 'apus_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 apus_hide_existing_comments($comments) {
return array();
}
add_filter('comments_array', 'apus_hide_existing_comments', 10, 2);
/**
* Desactivar feeds de comentarios
*
* Remueve los enlaces de feeds de comentarios del head.
*
* @since 1.0.0
*/
function apus_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', 'apus_disable_feed_comments');
add_action('do_feed_atom_comments', 'apus_disable_feed_comments');
}
add_action('init', 'apus_disable_comment_feeds');
/**
* Retornar error en feeds de comentarios
*
* @since 1.0.0
*/
function apus_disable_feed_comments() {
wp_die(
esc_html__('Los comentarios están desactivados en este sitio.', 'apus-theme'),
esc_html__('Comentarios no disponibles', 'apus-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 apus_disable_comment_reply_script() {
wp_deregister_script('comment-reply');
}
add_action('wp_enqueue_scripts', 'apus_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 apus_remove_comments_admin_menu() {
remove_menu_page('edit-comments.php');
}
add_action('admin_menu', 'apus_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 apus_remove_comments_admin_bar($wp_admin_bar) {
$wp_admin_bar->remove_menu('comments');
}
add_action('admin_bar_menu', 'apus_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 apus_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', 'apus_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 apus_remove_comments_column($columns) {
unset($columns['comments']);
return $columns;
}
// Aplicar a posts y páginas
add_filter('manage_posts_columns', 'apus_remove_comments_column');
add_filter('manage_pages_columns', 'apus_remove_comments_column');
/**
* Desactivar widgets de comentarios
*
* Remueve los widgets relacionados con comentarios.
*
* @since 1.0.0
*/
function apus_disable_comments_widgets() {
unregister_widget('WP_Widget_Recent_Comments');
}
add_action('widgets_init', 'apus_disable_comments_widgets');
/**
* Remover estilos CSS de comentarios recientes
*
* Remueve los estilos inline del widget de comentarios recientes.
*
* @since 1.0.0
*/
function apus_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', 'apus_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 apus_redirect_comment_urls() {
if (is_comment_feed()) {
wp_safe_redirect(home_url(), 301);
exit;
}
}
add_action('template_redirect', 'apus_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 apus_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', 'apus_disable_comments_rest_api');
/**
* Ocultar opciones de comentarios en el dashboard
*
* Remueve metaboxes de comentarios del dashboard.
*
* @since 1.0.0
*/
function apus_remove_dashboard_comments() {
remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal');
}
add_action('admin_init', 'apus_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 apus_disable_comment_emails() {
return false;
}
add_filter('notify_post_author', 'apus_disable_comment_emails', 10, 2);
add_filter('notify_moderator', 'apus_disable_comment_emails', 10, 2);

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

@@ -0,0 +1,367 @@
<?php
/**
* Critical CSS Generator and Inline Loader
*
* This file provides functionality to inline critical CSS for above-the-fold content,
* improving First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
*
* IMPORTANT: This feature is DISABLED by default. Enable it via Customizer.
*
* How it works:
* 1. When enabled, critical CSS is inlined in the <head>
* 2. Main stylesheet is loaded asynchronously after page load
* 3. Improves Core Web Vitals by reducing render-blocking CSS
*
* @package Apus_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 apus_is_critical_css_enabled() {
return get_theme_mod( 'apus_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 apus_get_critical_css() {
// Define critical CSS based on page type
$critical_css = '';
// Get transient to cache critical CSS
$transient_key = 'apus_critical_css_' . apus_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 = apus_get_home_critical_css();
} elseif ( is_single() ) {
$critical_css = apus_get_single_critical_css();
} elseif ( is_archive() || is_category() || is_tag() ) {
$critical_css = apus_get_archive_critical_css();
} else {
$critical_css = apus_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 apus_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 apus_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 apus_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 apus_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 apus_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 apus_output_critical_css() {
if ( ! apus_is_critical_css_enabled() ) {
return;
}
$critical_css = apus_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="apus-critical-css">' . $critical_css . '</style>' . "\n";
}
add_action( 'wp_head', 'apus_output_critical_css', 1 );
/**
* Load main stylesheet asynchronously when critical CSS is enabled
*
* @since 1.0.0
*/
function apus_async_main_stylesheet() {
if ( ! apus_is_critical_css_enabled() ) {
return;
}
// Dequeue main stylesheet to prevent render-blocking
wp_dequeue_style( 'apus-theme-style' );
// Enqueue with media="print" and onload to load asynchronously
wp_enqueue_style(
'apus-theme-style-async',
get_stylesheet_uri(),
array(),
APUS_VERSION,
'print'
);
// Add onload attribute to switch media to "all"
add_filter( 'style_loader_tag', 'apus_add_async_attribute', 10, 2 );
}
add_action( 'wp_enqueue_scripts', 'apus_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 apus_add_async_attribute( $html, $handle ) {
if ( 'apus-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 apus_critical_css_customizer( $wp_customize ) {
// Add Performance section
$wp_customize->add_section(
'apus_performance',
array(
'title' => __( 'Performance Optimization', 'apus-theme' ),
'priority' => 130,
)
);
// Critical CSS Enable/Disable
$wp_customize->add_setting(
'apus_enable_critical_css',
array(
'default' => false,
'sanitize_callback' => 'apus_sanitize_checkbox',
'transport' => 'refresh',
)
);
$wp_customize->add_control(
'apus_enable_critical_css',
array(
'label' => __( 'Enable Critical CSS', 'apus-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.', 'apus-theme' ),
'section' => 'apus_performance',
'type' => 'checkbox',
)
);
}
add_action( 'customize_register', 'apus_critical_css_customizer' );
/**
* Clear critical CSS cache when theme is updated
*
* @since 1.0.0
*/
function apus_clear_critical_css_cache() {
$page_types = array( 'home', 'single', 'archive', 'search', '404', 'page' );
foreach ( $page_types as $type ) {
delete_transient( 'apus_critical_css_' . $type );
}
}
add_action( 'after_switch_theme', 'apus_clear_critical_css_cache' );
add_action( 'customize_save_after', 'apus_clear_critical_css_cache' );

213
inc/cta-ab-testing.php Normal file
View File

@@ -0,0 +1,213 @@
<?php
/**
* CTA A/B Testing System
*
* Sistema de Call-to-Action con A/B Testing que muestra aleatoriamente
* una de dos variantes (A o B) para optimizar conversiones.
*
* Características:
* - Rotación 50/50 entre variante A (Catálogo) y B (Membresía)
* - Cookie persistence para mantener la misma variante por usuario
* - Template tag: apus_display_cta()
* - Tracking de conversiones con Google Analytics 4
*
* @package APUS_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Obtener la variante de CTA para el usuario actual
*
* Usa cookies para mantener la misma variante durante 30 días.
* Si no hay cookie, asigna aleatoriamente A o B (50/50).
*
* @return string 'A' o 'B'
*/
function apus_get_cta_variant() {
$cookie_name = 'apus_cta_variant';
// Verificar si ya existe una variante asignada
if (isset($_COOKIE[$cookie_name]) && in_array($_COOKIE[$cookie_name], array('A', 'B'))) {
return sanitize_text_field($_COOKIE[$cookie_name]);
}
// Asignar variante aleatoria (50/50)
$variant = (rand(0, 1) === 0) ? 'A' : 'B';
// Guardar en cookie por 30 días
setcookie($cookie_name, $variant, time() + (30 * DAY_IN_SECONDS), COOKIEPATH, COOKIE_DOMAIN);
return $variant;
}
/**
* Template tag para mostrar el CTA
*
* Uso: <?php apus_display_cta(); ?>
*
* @param array $args Argumentos opcionales para personalizar el CTA
* @return void
*/
function apus_display_cta($args = array()) {
// Verificar si el CTA está habilitado
$enable_cta = get_theme_mod('apus_enable_cta', true);
if (!$enable_cta) {
return;
}
// Solo mostrar en posts individuales por defecto
$show_on = isset($args['show_on']) ? $args['show_on'] : 'single';
if ($show_on === 'single' && !is_single()) {
return;
}
// Obtener la variante del usuario
$variant = apus_get_cta_variant();
// Obtener configuración desde el Customizer
$cta_config = apus_get_cta_config($variant);
// Renderizar el CTA
apus_render_cta($variant, $cta_config);
}
/**
* Obtener configuración del CTA desde el Customizer
*
* @param string $variant 'A' o 'B'
* @return array Configuración del CTA
*/
function apus_get_cta_config($variant) {
if ($variant === 'A') {
return array(
'title' => get_theme_mod('apus_cta_a_title', __('Accede a 200,000+ Análisis de Precios Unitarios', 'apus-theme')),
'text' => get_theme_mod('apus_cta_a_text', __('Consulta estructuras completas, insumos y dosificaciones de los APUs más utilizados en construcción en México.', 'apus-theme')),
'button_text' => get_theme_mod('apus_cta_a_button', __('Ver Catálogo Completo', 'apus-theme')),
'button_url' => get_theme_mod('apus_cta_a_url', home_url('/catalogo')),
'variant' => 'A',
);
} else {
return array(
'title' => get_theme_mod('apus_cta_b_title', __('¿Necesitas Consultar Más APUs?', 'apus-theme')),
'text' => get_theme_mod('apus_cta_b_text', __('Accede a nuestra biblioteca de 200,000 análisis de precios unitarios con estructuras detalladas y listados de insumos.', 'apus-theme')),
'button_text' => get_theme_mod('apus_cta_b_button', __('Conocer Planes de Membresía', 'apus-theme')),
'button_url' => get_theme_mod('apus_cta_b_url', home_url('/planes')),
'variant' => 'B',
);
}
}
/**
* Renderizar el HTML del CTA
*
* @param string $variant 'A' o 'B'
* @param array $config Configuración del CTA
* @return void
*/
function apus_render_cta($variant, $config) {
?>
<!-- CTA A/B Testing - Variante <?php echo esc_attr($variant); ?> -->
<div class="my-5 p-4 rounded cta-section cta-variant-<?php echo esc_attr(strtolower($variant)); ?>"
data-variant="<?php echo esc_attr($variant); ?>">
<div class="row align-items-center">
<div class="col-md-8">
<h3 class="h4 fw-bold text-white mb-2">
<?php echo esc_html($config['title']); ?>
</h3>
<p class="text-white mb-md-0">
<?php echo esc_html($config['text']); ?>
</p>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
<a href="<?php echo esc_url($config['button_url']); ?>"
class="btn btn-light btn-lg cta-button"
data-cta-variant="<?php echo esc_attr($variant); ?>">
<?php echo esc_html($config['button_text']); ?>
<i class="bi bi-arrow-right ms-2"></i>
</a>
</div>
</div>
</div>
<?php
}
/**
* Hook para agregar el CTA automáticamente después del contenido
*
* Se puede desactivar usando remove_filter('the_content', 'apus_auto_insert_cta')
*/
function apus_auto_insert_cta($content) {
// Solo en posts individuales
if (!is_single()) {
return $content;
}
// Verificar si está habilitado
$enable_cta = get_theme_mod('apus_enable_cta', true);
$auto_insert = get_theme_mod('apus_cta_auto_insert', false);
if (!$enable_cta || !$auto_insert) {
return $content;
}
// Capturar el output del CTA
ob_start();
apus_display_cta();
$cta_html = ob_get_clean();
// Insertar después del contenido
return $content . $cta_html;
}
// add_filter('the_content', 'apus_auto_insert_cta', 20); // Descomentado por defecto, usar template tag
/**
* Shortcode para insertar el CTA manualmente
*
* Uso: [apus_cta]
*/
function apus_cta_shortcode($atts) {
ob_start();
apus_display_cta($atts);
return ob_get_clean();
}
add_shortcode('apus_cta', 'apus_cta_shortcode');
/**
* Agregar atributos data-* al body para tracking
*/
function apus_add_cta_body_class($classes) {
if (is_single() && get_theme_mod('apus_enable_cta', true)) {
$variant = apus_get_cta_variant();
$classes[] = 'has-cta';
$classes[] = 'cta-variant-' . strtolower($variant);
}
return $classes;
}
add_filter('body_class', 'apus_add_cta_body_class');
/**
* Agregar datos de configuración para JavaScript
*/
function apus_cta_localize_script() {
if (!is_single() || !get_theme_mod('apus_enable_cta', true)) {
return;
}
$variant = apus_get_cta_variant();
$cta_data = array(
'variant' => $variant,
'ga_enabled' => !empty(get_theme_mod('apus_ga_tracking_id', '')),
'ga_id' => get_theme_mod('apus_ga_tracking_id', ''),
'debug_mode' => defined('WP_DEBUG') && WP_DEBUG,
);
wp_localize_script('apus-cta-tracking', 'apusCTA', $cta_data);
}
add_action('wp_enqueue_scripts', 'apus_cta_localize_script', 20);

257
inc/customizer-cta.php Normal file
View File

@@ -0,0 +1,257 @@
<?php
/**
* CTA A/B Testing Customizer Settings
*
* Opciones del panel de personalización para configurar
* las dos variantes del CTA (A y B).
*
* @package APUS_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Registrar configuraciones del CTA en el Customizer
*/
function apus_customize_cta($wp_customize) {
// Agregar sección para CTA A/B Testing
$wp_customize->add_section('apus_cta', array(
'title' => __('CTA A/B Testing', 'apus-theme'),
'description' => __('Configura las dos variantes del Call-to-Action que se mostrarán aleatoriamente. El sistema asignará automáticamente una variante a cada usuario (50/50).', 'apus-theme'),
'priority' => 132,
));
// =====================================================
// CONFIGURACIÓN GENERAL
// =====================================================
// Habilitar/Deshabilitar CTA
$wp_customize->add_setting('apus_enable_cta', array(
'default' => true,
'sanitize_callback' => 'apus_sanitize_checkbox',
'transport' => 'refresh',
));
$wp_customize->add_control('apus_enable_cta', array(
'label' => __('Habilitar CTA con A/B Testing', 'apus-theme'),
'description' => __('Muestra un Call-to-Action en los posts individuales con dos variantes aleatorias.', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'checkbox',
));
// Auto-insertar CTA (opcional, por defecto usar template tag)
$wp_customize->add_setting('apus_cta_auto_insert', array(
'default' => false,
'sanitize_callback' => 'apus_sanitize_checkbox',
'transport' => 'refresh',
));
$wp_customize->add_control('apus_cta_auto_insert', array(
'label' => __('Auto-insertar CTA después del contenido', 'apus-theme'),
'description' => __('Si está desactivado, usa el template tag apus_display_cta() manualmente.', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'checkbox',
));
// =====================================================
// VARIANTE A - ENFOQUE EN CATÁLOGO
// =====================================================
// Separador visual
$wp_customize->add_setting('apus_cta_a_separator', array(
'sanitize_callback' => 'sanitize_text_field',
));
$wp_customize->add_control(new WP_Customize_Control(
$wp_customize,
'apus_cta_a_separator',
array(
'label' => __('━━━ Variante A: Catálogo ━━━', 'apus-theme'),
'description' => __('Enfoque en acceso al catálogo de 200,000+ APUs', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'hidden',
)
));
// Título Variante A
$wp_customize->add_setting('apus_cta_a_title', array(
'default' => __('Accede a 200,000+ Análisis de Precios Unitarios', 'apus-theme'),
'sanitize_callback' => 'sanitize_text_field',
'transport' => 'postMessage',
));
$wp_customize->add_control('apus_cta_a_title', array(
'label' => __('Título', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'text',
));
// Texto Variante A
$wp_customize->add_setting('apus_cta_a_text', array(
'default' => __('Consulta estructuras completas, insumos y dosificaciones de los APUs más utilizados en construcción en México.', 'apus-theme'),
'sanitize_callback' => 'sanitize_textarea_field',
'transport' => 'postMessage',
));
$wp_customize->add_control('apus_cta_a_text', array(
'label' => __('Texto descriptivo', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'textarea',
));
// Botón Variante A
$wp_customize->add_setting('apus_cta_a_button', array(
'default' => __('Ver Catálogo Completo', 'apus-theme'),
'sanitize_callback' => 'sanitize_text_field',
'transport' => 'postMessage',
));
$wp_customize->add_control('apus_cta_a_button', array(
'label' => __('Texto del botón', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'text',
));
// URL Variante A
$wp_customize->add_setting('apus_cta_a_url', array(
'default' => '#',
'sanitize_callback' => 'esc_url_raw',
'transport' => 'postMessage',
));
$wp_customize->add_control('apus_cta_a_url', array(
'label' => __('URL del botón', 'apus-theme'),
'description' => __('Ejemplo: /catalogo-completo/ o una URL completa', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'url',
));
// =====================================================
// VARIANTE B - ENFOQUE EN MEMBRESÍA
// =====================================================
// Separador visual
$wp_customize->add_setting('apus_cta_b_separator', array(
'sanitize_callback' => 'sanitize_text_field',
));
$wp_customize->add_control(new WP_Customize_Control(
$wp_customize,
'apus_cta_b_separator',
array(
'label' => __('━━━ Variante B: Membresía ━━━', 'apus-theme'),
'description' => __('Enfoque en planes de membresía y acceso premium', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'hidden',
)
));
// Título Variante B
$wp_customize->add_setting('apus_cta_b_title', array(
'default' => __('¿Necesitas Consultar Más APUs?', 'apus-theme'),
'sanitize_callback' => 'sanitize_text_field',
'transport' => 'postMessage',
));
$wp_customize->add_control('apus_cta_b_title', array(
'label' => __('Título', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'text',
));
// Texto Variante B
$wp_customize->add_setting('apus_cta_b_text', array(
'default' => __('Accede a nuestra biblioteca de 200,000 análisis de precios unitarios con estructuras detalladas y listados de insumos.', 'apus-theme'),
'sanitize_callback' => 'sanitize_textarea_field',
'transport' => 'postMessage',
));
$wp_customize->add_control('apus_cta_b_text', array(
'label' => __('Texto descriptivo', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'textarea',
));
// Botón Variante B
$wp_customize->add_setting('apus_cta_b_button', array(
'default' => __('Conocer Planes de Membresía', 'apus-theme'),
'sanitize_callback' => 'sanitize_text_field',
'transport' => 'postMessage',
));
$wp_customize->add_control('apus_cta_b_button', array(
'label' => __('Texto del botón', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'text',
));
// URL Variante B
$wp_customize->add_setting('apus_cta_b_url', array(
'default' => '#',
'sanitize_callback' => 'esc_url_raw',
'transport' => 'postMessage',
));
$wp_customize->add_control('apus_cta_b_url', array(
'label' => __('URL del botón', 'apus-theme'),
'description' => __('Ejemplo: /planes-de-membresia/ o una URL completa', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'url',
));
// =====================================================
// GOOGLE ANALYTICS TRACKING
// =====================================================
// Separador visual
$wp_customize->add_setting('apus_cta_ga_separator', array(
'sanitize_callback' => 'sanitize_text_field',
));
$wp_customize->add_control(new WP_Customize_Control(
$wp_customize,
'apus_cta_ga_separator',
array(
'label' => __('━━━ Google Analytics ━━━', 'apus-theme'),
'description' => __('Configuración para tracking de conversiones', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'hidden',
)
));
// Google Analytics Tracking ID
$wp_customize->add_setting('apus_ga_tracking_id', array(
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
'transport' => 'refresh',
));
$wp_customize->add_control('apus_ga_tracking_id', array(
'label' => __('Google Analytics Tracking ID', 'apus-theme'),
'description' => __('Formato: G-XXXXXXXXXX (GA4) o UA-XXXXXXXXX-X (Universal Analytics). Déjalo vacío si ya tienes GA instalado mediante plugin.', 'apus-theme'),
'section' => 'apus_cta',
'type' => 'text',
));
}
add_action('customize_register', 'apus_customize_cta');
/**
* Agregar script de Google Analytics en el header si está configurado
*/
function apus_output_google_analytics() {
$tracking_id = get_theme_mod('apus_ga_tracking_id', '');
// No mostrar si está vacío o si estamos en el admin
if (empty($tracking_id) || is_admin()) {
return;
}
// No mostrar si es un usuario admin logueado
if (current_user_can('manage_options')) {
return;
}
?>
<!-- Google Analytics (CTA A/B Testing) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo esc_attr($tracking_id); ?>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '<?php echo esc_js($tracking_id); ?>', {
'anonymize_ip': true,
'cookie_flags': 'SameSite=None;Secure'
});
</script>
<?php
}
add_action('wp_head', 'apus_output_google_analytics', 1);

145
inc/customizer-fonts.php Normal file
View File

@@ -0,0 +1,145 @@
<?php
/**
* Font Options for Theme Customizer
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Register font settings in the Customizer
*/
function apus_customize_register_fonts($wp_customize) {
// Add Typography Section
$wp_customize->add_section('apus_typography', array(
'title' => __('Typography', 'apus-theme'),
'description' => __('Configure font settings for your site.', 'apus-theme'),
'priority' => 30,
));
// Setting: Use Custom Fonts
$wp_customize->add_setting('apus_use_custom_fonts', array(
'default' => false,
'sanitize_callback' => 'apus_sanitize_checkbox',
'transport' => 'refresh',
));
$wp_customize->add_control('apus_use_custom_fonts', array(
'label' => __('Use Custom Fonts', 'apus-theme'),
'description' => __('Enable custom fonts instead of system fonts. System fonts load faster and improve Core Web Vitals.', 'apus-theme'),
'section' => 'apus_typography',
'type' => 'checkbox',
));
// Setting: Font Loading Strategy (only if custom fonts enabled)
$wp_customize->add_setting('apus_font_display', array(
'default' => 'swap',
'sanitize_callback' => 'apus_sanitize_select',
'transport' => 'refresh',
));
$wp_customize->add_control('apus_font_display', array(
'label' => __('Font Display Strategy', 'apus-theme'),
'description' => __('Controls how fonts are displayed during loading. "swap" is recommended for best performance.', 'apus-theme'),
'section' => 'apus_typography',
'type' => 'select',
'choices' => array(
'auto' => __('Auto', 'apus-theme'),
'block' => __('Block', 'apus-theme'),
'swap' => __('Swap (Recommended)', 'apus-theme'),
'fallback' => __('Fallback', 'apus-theme'),
'optional' => __('Optional', 'apus-theme'),
),
'active_callback' => 'apus_is_custom_fonts_enabled',
));
// Setting: Preload Fonts
$wp_customize->add_setting('apus_preload_fonts', array(
'default' => true,
'sanitize_callback' => 'apus_sanitize_checkbox',
'transport' => 'refresh',
));
$wp_customize->add_control('apus_preload_fonts', array(
'label' => __('Preload Font Files', 'apus-theme'),
'description' => __('Preload critical font files for faster rendering. Recommended for better LCP scores.', 'apus-theme'),
'section' => 'apus_typography',
'type' => 'checkbox',
'active_callback' => 'apus_is_custom_fonts_enabled',
));
}
add_action('customize_register', 'apus_customize_register_fonts');
/**
* Check if custom fonts are enabled
*/
function apus_is_custom_fonts_enabled() {
return get_theme_mod('apus_use_custom_fonts', false);
}
/**
* Add body class based on font settings
*/
function apus_font_body_class($classes) {
if (apus_is_custom_fonts_enabled()) {
$classes[] = 'use-custom-fonts';
} else {
$classes[] = 'use-system-fonts';
}
return $classes;
}
add_filter('body_class', 'apus_font_body_class');
/**
* Add preload links for custom fonts
*/
function apus_preload_custom_fonts() {
// Only preload if custom fonts are enabled and preload is enabled
if (!apus_is_custom_fonts_enabled()) {
return;
}
if (!get_theme_mod('apus_preload_fonts', true)) {
return;
}
// Example preload links - uncomment and modify when you have custom font files
/*
?>
<link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/assets/fonts/CustomSans-Regular.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="<?php echo esc_url(get_template_directory_uri() . '/assets/fonts/CustomSans-Bold.woff2'); ?>" as="font" type="font/woff2" crossorigin>
<?php
*/
}
add_action('wp_head', 'apus_preload_custom_fonts', 1);
/**
* Output inline CSS for font display strategy
*/
function apus_output_font_display_css() {
if (!apus_is_custom_fonts_enabled()) {
return;
}
$font_display = get_theme_mod('apus_font_display', 'swap');
// This would be used if you have actual @font-face declarations
// For now, it's just a placeholder for future implementation
?>
<style id="apus-font-display-strategy">
/* Font display strategy: <?php echo esc_attr($font_display); ?> */
/* This would contain dynamic @font-face rules when custom fonts are added */
</style>
<?php
}
add_action('wp_head', 'apus_output_font_display_css', 5);

617
inc/enqueue-scripts.php Normal file
View File

@@ -0,0 +1,617 @@
<?php
/**
* Enqueue Bootstrap 5 and Custom Scripts
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Enqueue typography system
*/
function apus_enqueue_fonts() {
// Google Fonts - Poppins (según documentación INTRODUCCION.md)
wp_enqueue_style(
'google-fonts-poppins',
'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap',
array(),
null,
'all'
);
// Fonts CSS local
wp_enqueue_style(
'apus-fonts',
get_template_directory_uri() . '/assets/css/css-global-fonts.css',
array('google-fonts-poppins'),
'1.0.0',
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_fonts', 1);
/**
* Enqueue Bootstrap 5 styles and scripts
*/
function apus_enqueue_bootstrap() {
// Bootstrap CSS - with high priority
wp_enqueue_style(
'apus-bootstrap',
get_template_directory_uri() . '/assets/vendor/bootstrap/css/bootstrap.min.css',
array('apus-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('apus-bootstrap'),
'1.11.3',
'all'
);
// Variables CSS del Template RDash (NIVEL 1 - BLOQUEANTE - Issue #48)
wp_enqueue_style(
'apus-variables',
get_template_directory_uri() . '/assets/css/css-global-variables.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
// Bootstrap JS Bundle - in footer with defer
wp_enqueue_script(
'apus-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', 'apus_enqueue_bootstrap', 5);
/**
* Enqueue main theme stylesheet
* FASE 1 - Este es el archivo CSS principal del tema
*/
function apus_enqueue_main_stylesheet() {
wp_enqueue_style(
'apus-main-style',
get_template_directory_uri() . '/assets/css/style.css',
array('apus-variables'),
'1.0.5', // Arquitectura: Separación de responsabilidades CSS
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_main_stylesheet', 5);
/**
* Enqueue FASE 2 CSS - Template RDash Component Styles (Issues #58-64)
*
* Estilos que replican componentes del template RDash
*/
function apus_enqueue_fase2_styles() {
// Hero Section CSS - Gradiente azul (Issue #59)
wp_enqueue_style(
'apus-hero',
get_template_directory_uri() . '/assets/css/componente-hero-section.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-hero-section.css'),
'all'
);
// Category Badges CSS - Clase genérica (Issue #62)
wp_enqueue_style(
'apus-badges',
get_template_directory_uri() . '/assets/css/css-global-badges.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/css-global-badges.css'),
'all'
);
// Pagination CSS - Estilos personalizados (Issue #64)
wp_enqueue_style(
'apus-pagination',
get_template_directory_uri() . '/assets/css/css-global-pagination.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/css-global-pagination.css'),
'all'
);
// Post Content Typography - Solo en posts individuales (Issue #63)
if (is_single()) {
wp_enqueue_style(
'apus-post-content',
get_template_directory_uri() . '/assets/css/componente-post-content.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-post-content.css'),
'all'
);
// Related Posts CSS - Background gris (Issue #60)
wp_enqueue_style(
'apus-related-posts',
get_template_directory_uri() . '/assets/css/componente-related-posts.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-related-posts.css'),
'all'
);
}
}
add_action('wp_enqueue_scripts', 'apus_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 apus_enqueue_global_components() {
// Notification Bar CSS - Barra superior (Issue #39)
wp_enqueue_style(
'apus-notification-bar',
get_template_directory_uri() . '/assets/css/componente-top-bar.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-top-bar.css'),
'all'
);
// Navbar CSS - Navegación principal
wp_enqueue_style(
'apus-navbar',
get_template_directory_uri() . '/assets/css/componente-navbar.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-navbar.css'),
'all'
);
// Buttons CSS - Botones personalizados (Let's Talk, etc.)
wp_enqueue_style(
'apus-buttons',
get_template_directory_uri() . '/assets/css/componente-boton-lets-talk.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-boton-lets-talk.css'),
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_global_components', 7);
/**
* Enqueue header styles and scripts
*/
function apus_enqueue_header() {
// Header CSS
wp_enqueue_style(
'apus-header',
get_template_directory_uri() . '/assets/css/componente-footer-principal.css',
array('apus-fonts'),
'1.0.0',
'all'
);
// Header JS - with defer strategy
wp_enqueue_script(
'apus-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', 'apus_enqueue_header', 10);
/**
* Enqueue generic tables styles
*
* ARQUITECTURA: Estilos para tablas genéricas en post-content
* Solo en posts individuales
*/
function apus_enqueue_generic_tables() {
// Only enqueue on single posts
if (!is_single()) {
return;
}
// Generic Tables CSS
wp_enqueue_style(
'apus-generic-tables',
get_template_directory_uri() . '/assets/css/css-global-generic-tables.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_generic_tables', 11);
/**
* Enqueue video iframe styles
*
* ARQUITECTURA: Estilos para videos embebidos (YouTube, Vimeo)
* Solo en posts individuales
*/
function apus_enqueue_video_styles() {
// Only enqueue on single posts
if (!is_single()) {
return;
}
// Video CSS
wp_enqueue_style(
'apus-video',
get_template_directory_uri() . '/assets/css/css-global-video.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_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 apus_enqueue_main_javascript() {
// Main JavaScript - navbar scroll effects and interactions
wp_enqueue_script(
'apus-main-js',
get_template_directory_uri() . '/assets/js/main.js',
array('apus-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(
'apus-main-js',
'apusTheme',
array(
'themeUrl' => get_template_directory_uri(),
'ajaxUrl' => admin_url('admin-ajax.php'),
)
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_main_javascript', 11);
/**
* Remove defer from apus-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 apus_remove_defer_from_main_js($tag, $handle) {
if ('apus-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', 'apus_remove_defer_from_main_js', 20, 2);
/**
* ELIMINADO: apus_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 apus_enqueue_accessibility() {
// Accessibility CSS
wp_enqueue_style(
'apus-accessibility',
get_template_directory_uri() . '/assets/css/css-global-accessibility.css',
array('apus-theme-style'),
APUS_VERSION,
'all'
);
// Accessibility JavaScript
wp_enqueue_script(
'apus-accessibility-js',
get_template_directory_uri() . '/assets/js/accessibility.js',
array('apus-bootstrap-js'),
APUS_VERSION,
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'apus_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 apus_enqueue_adsense_loader() {
// Solo ejecutar en frontend
if (is_admin()) {
return;
}
// Verificar si el retardo de AdSense está habilitado
$delay_enabled = apus_get_option('apus_adsense_delay_enabled', '1');
if ($delay_enabled !== '1') {
return;
}
// Enqueue del script de carga de AdSense
wp_enqueue_script(
'apus-adsense-loader',
get_template_directory_uri() . '/assets/js/adsense-loader.js',
array(),
APUS_VERSION,
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'apus_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 apus_enqueue_theme_styles() {
// Theme Core Styles - ELIMINADO theme.css
// wp_enqueue_style(
// 'apus-theme',
// get_template_directory_uri() . '/assets/css/theme.css',
// array('apus-bootstrap'),
// '1.0.0',
// 'all'
// );
// Theme Animations
wp_enqueue_style(
'apus-animations',
get_template_directory_uri() . '/assets/css/css-global-animations.css',
array('apus-bootstrap'), // Cambiado de 'apus-theme' a 'apus-bootstrap'
'1.0.0',
'all'
);
// Theme Responsive Styles
wp_enqueue_style(
'apus-responsive',
get_template_directory_uri() . '/assets/css/css-global-responsive.css',
array('apus-bootstrap'), // Cambiado de 'apus-theme' a 'apus-bootstrap'
'1.0.0',
'all'
);
// Theme Utilities
wp_enqueue_style(
'apus-utilities',
get_template_directory_uri() . '/assets/css/css-global-utilities.css',
array('apus-bootstrap'), // Cambiado de 'apus-theme' a 'apus-bootstrap'
'1.0.0',
'all'
);
// Print Styles
wp_enqueue_style(
'apus-print',
get_template_directory_uri() . '/assets/css/css-global-print.css',
array(),
'1.0.0',
'print'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_theme_styles', 13);
/**
* Enqueue social share styles
*
* HABILITADO: CSS de share buttons debe estar en su propio archivo
* Arquitectura correcta: cada componente tiene su archivo CSS individual
* Ver: wp-content/themes/apus-theme/assets/css/componente-share-buttons.css
*/
function apus_enqueue_social_share_styles() {
// Only enqueue on single posts
if (!is_single()) {
return;
}
// Social Share CSS
wp_enqueue_style(
'apus-social-share',
get_template_directory_uri() . '/assets/css/componente-share-buttons.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_social_share_styles', 14);
/**
* Enqueue APU Tables styles
*/
function apus_enqueue_apu_tables_styles() {
// APU Tables CSS
wp_enqueue_style(
'apus-tables-apu',
get_template_directory_uri() . '/assets/css/css-tablas-apu.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_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 apus_enqueue_apu_tables_autoclass_script() {
// APU Tables Auto-Class JS
wp_enqueue_script(
'apus-apu-tables-autoclass',
get_template_directory_uri() . '/assets/js/apu-tables-auto-class.js',
array(),
APUS_VERSION,
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_apu_tables_autoclass_script', 15);
/**
* Enqueue CTA A/B Testing styles and scripts
*/
function apus_enqueue_cta_assets() {
// Solo enqueue en posts individuales
if (!is_single()) {
return;
}
// Verificar si el CTA está habilitado
$enable_cta = get_theme_mod('apus_enable_cta', true);
if (!$enable_cta) {
return;
}
// CTA CSS
wp_enqueue_style(
'apus-cta-style',
get_template_directory_uri() . '/assets/css/componente-cta-ab-testing.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
// CTA Tracking JS
wp_enqueue_script(
'apus-cta-tracking',
get_template_directory_uri() . '/assets/js/cta-tracking.js',
array(),
APUS_VERSION,
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_cta_assets', 16);
/**
* Enqueue CTA Box Sidebar styles (Issue #36)
*/
function apus_enqueue_cta_box_sidebar_assets() {
// Solo enqueue en posts individuales
if (!is_single()) {
return;
}
// CTA Box Sidebar CSS
wp_enqueue_style(
'apus-cta-box-sidebar',
get_template_directory_uri() . '/assets/css/componente-cta-box-sidebar.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-cta-box-sidebar.css'),
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_cta_box_sidebar_assets', 17);
/**
* Enqueue TOC Sidebar styles (only on single posts)
*
* ARQUITECTURA: Cada componente debe tener su propio archivo CSS
* Issue #121 - Separación de responsabilidades CSS
*
* @since 1.0.5
*/
function apus_enqueue_toc_sidebar_assets() {
// Only load on single posts
if (!is_single()) {
return;
}
// TOC Sidebar CSS
wp_enqueue_style(
'apus-toc-sidebar',
get_template_directory_uri() . '/assets/css/componente-sidebar-toc.css',
array('apus-bootstrap'),
filemtime(get_template_directory() . '/assets/css/componente-sidebar-toc.css'),
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_toc_sidebar_assets', 18);
/**
* Enqueue Footer Contact Form styles
*
* ARQUITECTURA CORRECTA: Cada componente debe tener su propio archivo CSS
* Footer Contact Form CSS ahora está en su archivo individual
* Ver: wp-content/themes/apus-theme/assets/css/componente-footer-contact-form.css
*/
function apus_enqueue_footer_contact_assets() {
// Footer Contact CSS
wp_enqueue_style(
'apus-footer-contact',
get_template_directory_uri() . '/assets/css/componente-footer-contact-form.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_footer_contact_assets', 18);

478
inc/featured-image.php Normal file
View File

@@ -0,0 +1,478 @@
<?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 Apus_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:
* - apus-featured-large: 1200x600 (para single posts)
* - apus-featured-medium: 800x400 (para archives)
* - apus-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: apus-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 apus_get_featured_image($post_id = null, $size = 'apus-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 según tipo de contenido
if (!$force_show) {
// Primero intentar con apus_get_option (sistema de opciones del tema)
if (function_exists('apus_get_option')) {
if ($post_type === 'post') {
$enabled = apus_get_option('featured_image_single', true);
} elseif ($post_type === 'page') {
$enabled = apus_get_option('featured_image_page', true);
} else {
$enabled = apus_get_option('featured_image_' . $post_type, true);
}
} else {
// Fallback a theme_mod
$option_key = 'apus_featured_image_' . $post_type;
$enabled = get_theme_mod($option_key, true);
}
if (!$enabled) {
return '';
}
}
// 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 apus_get_featured_image().
*
* Uso en templates:
* <?php apus_the_featured_image(); ?>
* <?php apus_the_featured_image(null, 'apus-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 apus_the_featured_image($post_id = null, $size = 'apus-featured-large', $attr = array(), $force_show = false) {
echo apus_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 apus_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, 'apus-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 apus_get_post_thumbnail().
*
* Uso en templates:
* <?php apus_the_post_thumbnail(); ?>
* <?php apus_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 apus_the_post_thumbnail($post_id = null, $with_link = true) {
echo apus_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 apus-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 apus_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, 'apus-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 apus_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 apus_the_post_thumbnail_small($post_id = null, $with_link = true) {
echo apus_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 (apus_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 apus_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);
// Verificar configuración global según tipo de contenido
if (function_exists('apus_get_option')) {
if ($post_type === 'post') {
$enabled = apus_get_option('featured_image_single', true);
} elseif ($post_type === 'page') {
$enabled = apus_get_option('featured_image_page', true);
} else {
$enabled = apus_get_option('featured_image_' . $post_type, true);
}
} else {
// Fallback a theme_mod
$option_key = 'apus_featured_image_' . $post_type;
$enabled = get_theme_mod($option_key, true);
}
return (bool) $enabled;
}
/**
* Obtiene la URL de la imagen destacada
*
* Útil para backgrounds CSS o meta tags de redes sociales (Open Graph, Twitter Cards).
*
* Uso:
* $image_url = apus_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: apus-featured-large)
* @return string URL de la imagen o cadena vacía
*/
function apus_get_featured_image_url($post_id = null, $size = 'apus-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 apus_get_featured_image_responsive(); ?>
* <?php echo apus_get_featured_image_responsive(null, 'apus-featured-medium', '16/9'); ?>
*
* @param int|null $post_id ID del post (null = post actual)
* @param string $size Tamaño de imagen registrado (default: apus-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 apus_get_featured_image_responsive($post_id = null, $size = 'apus-featured-large', $aspect_ratio = '2/1') {
// Obtener la imagen
$image = apus_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 apus_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 apus_the_featured_image_responsive($post_id = null, $size = 'apus-featured-large', $aspect_ratio = '2/1') {
echo apus_get_featured_image_responsive($post_id, $size, $aspect_ratio);
}
/**
* Verifica si las imágenes destacadas están habilitadas para un tipo de post
*
* Función legacy mantenida por compatibilidad.
*
* @param string $post_type Tipo de post (vacío = post actual)
* @return bool True si habilitadas, false en caso contrario
*/
function apus_is_featured_image_enabled($post_type = '') {
if (empty($post_type)) {
$post_type = get_post_type();
}
if (!$post_type) {
return true; // Default habilitado
}
// Intentar con apus_get_option primero
if (function_exists('apus_get_option')) {
if ($post_type === 'post') {
return apus_get_option('featured_image_single', true);
} elseif ($post_type === 'page') {
return apus_get_option('featured_image_page', true);
} else {
return apus_get_option('featured_image_' . $post_type, true);
}
}
// Fallback a theme_mod
$option_key = 'apus_featured_image_' . $post_type;
return (bool) get_theme_mod($option_key, true);
}
/**
* Registra configuración de imágenes destacadas en Customizer
*
* Agrega controles para habilitar/deshabilitar imágenes destacadas por tipo de post.
* Funciona como fallback si no hay panel de opciones del tema.
*
* @param WP_Customize_Manager $wp_customize Objeto Theme Customizer
*/
function apus_featured_image_customizer($wp_customize) {
// Solo agregar si no existe el panel de opciones del tema
if (function_exists('apus_get_option')) {
return; // El panel de opciones manejará esto
}
// Agregar sección
$wp_customize->add_section('apus_featured_images', array(
'title' => __('Imágenes Destacadas', 'apus-theme'),
'description' => __('Configurar visualización de imágenes destacadas por tipo de contenido.', 'apus-theme'),
'priority' => 30,
));
// Obtener tipos de post públicos
$post_types = get_post_types(array('public' => true), 'objects');
foreach ($post_types as $post_type) {
// Saltar attachments
if ($post_type->name === 'attachment') {
continue;
}
$setting_id = 'apus_featured_image_' . $post_type->name;
// Agregar setting
$wp_customize->add_setting($setting_id, array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
'transport' => 'refresh',
));
// Agregar control
$wp_customize->add_control($setting_id, array(
'label' => sprintf(
/* translators: %s: nombre del tipo de post */
__('Habilitar para %s', 'apus-theme'),
$post_type->labels->name
),
'section' => 'apus_featured_images',
'type' => 'checkbox',
));
}
}
add_action('customize_register', 'apus_featured_image_customizer');

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

@@ -0,0 +1,500 @@
<?php
/**
* Image Optimization Functions
*
* Handles responsive images, WebP/AVIF support, lazy loading, and image preloading.
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Enable AVIF image support
*/
function apus_enable_avif_support($mime_types) {
$mime_types['avif'] = 'image/avif';
return $mime_types;
}
add_filter('upload_mimes', 'apus_enable_avif_support');
/**
* Add AVIF to allowed file extensions
*/
function apus_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', 'apus_allow_avif_extension', 10, 4);
/**
* Configure custom image sizes
* Already defined in functions.php, but we can add more if needed
*/
function apus_setup_additional_image_sizes() {
// Add support for additional modern image sizes
add_image_size('apus-hero', 1920, 800, true); // Hero images
add_image_size('apus-card', 600, 400, true); // Card thumbnails
add_image_size('apus-thumbnail-2x', 800, 600, true); // Retina thumbnails
}
add_action('after_setup_theme', 'apus_setup_additional_image_sizes');
/**
* Add custom image sizes to media library dropdown
*/
function apus_custom_image_sizes($sizes) {
return array_merge($sizes, array(
'apus-thumbnail' => __('Thumbnail (400x300)', 'apus-theme'),
'apus-medium' => __('Medium (800x600)', 'apus-theme'),
'apus-large' => __('Large (1200x900)', 'apus-theme'),
'apus-featured-large' => __('Featured Large (1200x600)', 'apus-theme'),
'apus-featured-medium' => __('Featured Medium (800x400)', 'apus-theme'),
'apus-hero' => __('Hero (1920x800)', 'apus-theme'),
'apus-card' => __('Card (600x400)', 'apus-theme'),
));
}
add_filter('image_size_names_choose', 'apus_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 apus_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 apus_get_responsive_image($attachment_id, $size = 'full', $attr = array(), $lazy = true) {
if (empty($attachment_id)) {
return '';
}
// Get responsive attributes
$responsive_attrs = apus_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 apus_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', 'apus_add_lazy_loading_to_images', 10, 3);
/**
* Add lazy loading to content images
*/
function apus_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', 'apus_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 apus_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 apus_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 = 'apus-featured-large';
// Preload the image
apus_preload_image($thumbnail_id, $size);
}
add_action('wp_head', 'apus_preload_featured_image', 5);
/**
* Enable fetchpriority attribute for featured images
*/
function apus_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', 'apus_add_fetchpriority_to_featured_image', 20, 3);
/**
* Optimize image quality for uploads
*/
function apus_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', 'apus_optimize_image_quality', 10, 2);
add_filter('wp_editor_set_quality', 'apus_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 apus_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 apus_big_image_size_threshold($threshold) {
// Mantener 2560px como threshold (no desactivar completamente)
return 2560;
}
add_filter('big_image_size_threshold', 'apus_big_image_size_threshold');
/**
* Set maximum srcset image width
*/
function apus_max_srcset_image_width($max_width, $size_array) {
// Limit srcset to images up to 2560px wide
return 2560;
}
add_filter('max_srcset_image_width', 'apus_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 apus_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', 'apus_enable_webp_generation');
/**
* Configurar tipos MIME adicionales para WebP y AVIF
*/
function apus_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', 'apus_additional_mime_types');
/**
* Remover tamaños de imagen no utilizados para ahorrar espacio
*/
function apus_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', 'apus_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 apus_responsive_image_sizes_attr($sizes, $size, $image_src, $image_meta, $attachment_id) {
// Para imágenes destacadas grandes (single posts)
if ($size === 'apus-featured-large' || $size === 'apus-large') {
$sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px';
}
// Para imágenes destacadas medianas (archives)
elseif ($size === 'apus-featured-medium' || $size === 'apus-medium') {
$sizes = '(max-width: 768px) 100vw, (max-width: 992px) 50vw, 800px';
}
// Para thumbnails (widgets, related posts)
elseif ($size === 'apus-thumbnail') {
$sizes = '(max-width: 576px) 100vw, 400px';
}
// Para hero images
elseif ($size === 'apus-hero') {
$sizes = '100vw';
}
return $sizes;
}
add_filter('wp_calculate_image_sizes', 'apus_responsive_image_sizes_attr', 10, 5);
/**
* Forzar regeneración de metadatos de imagen para incluir WebP/AVIF
* Solo se ejecuta cuando sea necesario
*/
function apus_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', 'apus_maybe_regenerate_image_metadata', 10, 2);
/**
* Excluir lazy loading en la primera imagen del contenido (posible LCP)
*/
function apus_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', 'apus_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 apus_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', 'apus_enable_image_subsizes', 20, 3);

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

@@ -0,0 +1,200 @@
<?php
/**
* Bootstrap 5 Nav Walker
*
* Custom Walker para wp_nav_menu() compatible con Bootstrap 5.
* Genera markup correcto para navbar, dropdowns y menús responsive.
*
* @package Apus_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ú', 'apus-theme');
echo '</a>';
echo '</li>';
echo '</ul>';
}
}
}

599
inc/performance.php Normal file
View File

@@ -0,0 +1,599 @@
<?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 Apus_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 apus_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', 'apus_disable_emojis_tinymce' );
}
add_action( 'init', 'apus_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 apus_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 apus_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', 'apus_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', 'apus_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 apus_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 apus_dequeue_embed_script() {
wp_deregister_script( 'wp-embed' );
}
add_action( 'wp_footer', 'apus_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 apus_disable_feeds() {
wp_die(
esc_html__( 'No feed available, please visit our homepage!', 'apus' ),
esc_html__( 'Feed Not Available', 'apus' ),
array(
'response' => 404,
'back_link' => true,
)
);
}
/**
* Remove feed links and redirect feed URLs
*
* @since 1.0.0
*/
function apus_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', 'apus_disable_feeds', 1 );
add_action( 'do_feed_rdf', 'apus_disable_feeds', 1 );
add_action( 'do_feed_rss', 'apus_disable_feeds', 1 );
add_action( 'do_feed_rss2', 'apus_disable_feeds', 1 );
add_action( 'do_feed_atom', 'apus_disable_feeds', 1 );
add_action( 'do_feed_rss2_comments', 'apus_disable_feeds', 1 );
add_action( 'do_feed_atom_comments', 'apus_disable_feeds', 1 );
}
add_action( 'init', 'apus_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 apus_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', 'apus_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 apus_disable_dashicons() {
if ( ! is_user_logged_in() ) {
wp_deregister_style( 'dashicons' );
}
}
add_action( 'wp_enqueue_scripts', 'apus_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 apus_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', 'apus_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 apus_remove_wp_version() {
return '';
}
add_filter( 'the_generator', 'apus_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 apus_disable_xmlrpc() {
return false;
}
add_filter( 'xmlrpc_enabled', 'apus_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 apus_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', 'apus_remove_jquery_migrate' );
/**
* Optimize WordPress Database Queries
*
* Removes unnecessary meta queries for better performance.
*
* @since 1.0.0
*/
function apus_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', 'apus_optimize_queries' );
/**
* Disable WordPress Admin Bar for Non-Admins
*
* Reduces HTTP requests for non-admin users.
*
* @since 1.0.0
*/
function apus_disable_admin_bar() {
if ( ! current_user_can( 'administrator' ) && ! is_admin() ) {
show_admin_bar( false );
}
}
add_action( 'after_setup_theme', 'apus_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 apus_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', 'apus_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 apus_preload_critical_resources() {
// Preload de fuentes críticas
$fonts = array(
'inter-var.woff2',
'inter-var-italic.woff2',
);
foreach ( $fonts as $font ) {
$font_url = get_template_directory_uri() . '/assets/fonts/' . $font;
printf(
'<link rel="preload" href="%s" as="font" type="font/woff2" crossorigin="anonymous">' . "\n",
esc_url( $font_url )
);
}
// 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/fonts.css';
printf(
'<link rel="preload" href="%s" as="style">' . "\n",
esc_url( $fonts_css )
);
}
add_action( 'wp_head', 'apus_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 apus_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', 'apus_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 apus_optimize_heartbeat() {
// Desactivar completamente en el frontend
if ( ! is_admin() ) {
wp_deregister_script( 'heartbeat' );
}
}
add_action( 'init', 'apus_optimize_heartbeat', 1 );
/**
* Modificar configuración del Heartbeat en admin
*
* @since 1.0.0
* @param array $settings Heartbeat settings.
* @return array Modified settings.
*/
function apus_modify_heartbeat_settings( $settings ) {
// Cambiar intervalo de 15 segundos (default) a 60 segundos
$settings['interval'] = 60;
return $settings;
}
add_filter( 'heartbeat_settings', 'apus_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 apus_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', 'apus_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 apus_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', 'apus_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 apus_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', 'apus_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 apus_enable_image_dimensions() {
return true;
}
add_filter( 'wp_lazy_loading_enabled', 'apus_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 apus_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', 'apus_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 apus_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_apus_' ) . '%',
time()
)
);
}
// Ejecutar limpieza semanalmente
add_action( 'apus_weekly_cleanup', 'apus_cleanup_expired_transients' );
// Registrar evento cron si no existe
if ( ! wp_next_scheduled( 'apus_weekly_cleanup' ) ) {
wp_schedule_event( time(), 'weekly', 'apus_weekly_cleanup' );
}
/*
* NOTA: Funciones previamente deshabilitadas han sido reimplementadas
* con mejoras para evitar loops infinitos y problemas de memoria.
*
* - Resource hints (dns-prefetch, preconnect) - REACTIVADO
* - Preload de recursos críticos - REACTIVADO
* - Optimización del Heartbeat API - REACTIVADO
* - Remoción de query strings - REACTIVADO (solo para assets propios)
* - Script attributes (defer/async) - REACTIVADO
*/

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

@@ -0,0 +1,294 @@
<?php
/**
* Related Posts Functionality
*
* Provides configurable related posts functionality with Bootstrap grid support.
*
* @package Apus_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 apus_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('apus_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('apus_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 apus_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('apus_related_posts_enabled', true);
if (!$enabled) {
return;
}
// Get related posts
$related_query = apus_get_related_posts($post_id);
if (!$related_query) {
return;
}
// Get configuration options
$title = get_option('apus_related_posts_title', __('Related Posts', 'apus-theme'));
$columns = get_option('apus_related_posts_columns', 3);
$show_excerpt = get_option('apus_related_posts_show_excerpt', true);
$show_date = get_option('apus_related_posts_show_date', true);
$show_category = get_option('apus_related_posts_show_category', true);
$excerpt_length = get_option('apus_related_posts_excerpt_length', 20);
$background_colors = get_option('apus_related_posts_bg_colors', array(
'#1a73e8', // Blue
'#e91e63', // Pink
'#4caf50', // Green
'#ff9800', // Orange
'#9c27b0', // Purple
'#00bcd4', // Cyan
));
// Calculate Bootstrap column class
$col_class = apus_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('apus-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 apus_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 apus_hook_related_posts() {
if (is_single() && !is_attachment()) {
apus_display_related_posts();
}
}
add_action('apus_after_post_content', 'apus_hook_related_posts');
/**
* Enqueue related posts styles
*/
function apus_enqueue_related_posts_styles() {
if (is_single() && !is_attachment()) {
$enabled = get_option('apus_related_posts_enabled', true);
if ($enabled) {
wp_enqueue_style(
'apus-related-posts',
get_template_directory_uri() . '/assets/css/related-posts.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
}
}
}
add_action('wp_enqueue_scripts', 'apus_enqueue_related_posts_styles');
/**
* Register related posts settings
* These can be configured via theme options or customizer
*/
function apus_related_posts_default_options() {
// Set default options if they don't exist
$defaults = array(
'apus_related_posts_enabled' => true,
'apus_related_posts_title' => __('Related Posts', 'apus-theme'),
'apus_related_posts_count' => 3,
'apus_related_posts_columns' => 3,
'apus_related_posts_show_excerpt' => true,
'apus_related_posts_excerpt_length' => 20,
'apus_related_posts_show_date' => true,
'apus_related_posts_show_category' => true,
'apus_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', 'apus_related_posts_default_options');

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

@@ -0,0 +1,150 @@
<?php
/**
* Funciones de sanitización para el tema APUS
*
* Este archivo centraliza todas las funciones de sanitización utilizadas
* en el Customizer, panel de opciones y demás componentes del tema.
*
* @package APUS_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
if (!function_exists('apus_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 apus_sanitize_checkbox($input) {
return (bool) $input;
}
}
if (!function_exists('apus_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 apus_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('apus_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 apus_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('apus_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 apus_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('apus_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 apus_sanitize_integer($input) {
return absint($input);
}
}
if (!function_exists('apus_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 apus_sanitize_text($input) {
return sanitize_text_field($input);
}
}
if (!function_exists('apus_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 apus_sanitize_url($input) {
return esc_url_raw($input);
}
}
if (!function_exists('apus_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 apus_sanitize_html($input) {
return wp_kses_post($input);
}
}

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

@@ -0,0 +1,115 @@
<?php
/**
* Desactivar funcionalidad de búsqueda nativa de WordPress
*
* Este archivo desactiva completamente la búsqueda nativa de WordPress.
* Las rutas de búsqueda retornarán 404.
*
* Comportamiento:
* - Rutas de búsqueda (ej. /search/ o /?s=query desde raíz) → 404
* - URLs válidas con parámetro ?s= → entregar página normal, ignorar parámetro
*
* @package Apus_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 apus_disable_search_widget() {
unregister_widget('WP_Widget_Search');
}
add_action('widgets_init', 'apus_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 apus_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', 'apus_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 apus_disable_search_form() {
return '';
}
add_filter('get_search_form', 'apus_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 apus_noindex_search() {
if (is_search()) {
echo '<meta name="robots" content="noindex,nofollow">' . "\n";
}
}
add_action('wp_head', 'apus_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 apus_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', 'apus_remove_search_rewrite_rules');

205
inc/seo.php Normal file
View File

@@ -0,0 +1,205 @@
<?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 Apus_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 apus_remove_generator() {
return '';
}
remove_action('wp_head', 'wp_generator');
add_filter('the_generator', 'apus_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 apus_robots_header() {
if (is_robots()) {
header('Cache-Control: public, max-age=86400');
header('Expires: ' . gmdate('r', time() + 86400));
}
}
add_action('pre_handle_robots_txt', 'apus_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 apus_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 apus_seo_head_hooks() {
// This ensures proper hook execution order for Rank Math compatibility
do_action('apus_head_close');
}
/**
* Add prefetch hints for external resources
*
* Improves performance by preemptively connecting to external domains
* commonly used in the theme (fonts, CDNs, etc.)
*
* @since 1.0.0
*/
function apus_prefetch_external() {
// Google Fonts prefetch
echo '<link rel="preconnect" href="https://fonts.googleapis.com">' . "\n";
echo '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>' . "\n";
}
add_action('wp_head', 'apus_prefetch_external', 1);
/**
* 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 apus_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 apus_schema_fallback() {
// Only output schema if Rank Math is NOT active
if (apus_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" . '<!-- Apus 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', 'apus_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 apus_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', 'apus_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 apus_verify_title_tag_support() {
if (!current_theme_supports('title-tag')) {
// Log warning if title-tag support is somehow disabled
error_log('Warning: Apus Theme title-tag support not properly initialized');
}
}
add_action('after_setup_theme', 'apus_verify_title_tag_support', 20);

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

@@ -0,0 +1,127 @@
<?php
/**
* Social Share Buttons
*
* Funciones para mostrar botones de compartir en redes sociales
* en posts individuales. Utiliza URLs nativas sin dependencias de JavaScript.
*
* @package Apus_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 apus_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 las opciones del tema
$share_text = apus_get_option( 'apus_share_text', __( 'Compartir:', 'apus-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', 'apus-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', 'apus-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', 'apus-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', 'apus-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', 'apus-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 apus_display_social_share( $post_id = 0 ) {
// Solo mostrar en posts individuales
if ( ! is_single() ) {
return;
}
// Verificar si los botones de compartir están habilitados
$enable_share = apus_get_option( 'apus_enable_share_buttons', '1' );
if ( $enable_share !== '1' ) {
return;
}
// Mostrar los botones
echo apus_get_social_share_buttons( $post_id );
}

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

@@ -0,0 +1,458 @@
<?php
/**
* Template Functions
*
* Helper functions for theme templates.
*
* @package Apus_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 apus_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', 'apus' ),
'<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 apus_posted_by() {
$byline = sprintf(
/* translators: %s: post author. */
esc_html_x( 'por %s', 'post author', 'apus' ),
'<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 apus_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__( ', ', 'apus' ) );
if ( $categories_list ) {
/* translators: 1: list of categories. */
printf( '<span class="cat-links">' . esc_html__( 'Categorías: %1$s', 'apus' ) . '</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', 'apus' ) );
if ( $tags_list ) {
/* translators: 1: list of tags. */
printf( '<span class="tags-links">' . esc_html__( 'Etiquetas: %1$s', 'apus' ) . '</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>', 'apus' ),
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>', 'apus' ),
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 apus_excerpt_length( $length ) {
if ( is_admin() ) {
return $length;
}
return 40; // Change this to desired excerpt length.
}
add_filter( 'excerpt_length', 'apus_excerpt_length', 999 );
/**
* Custom excerpt more string
*
* @since 1.0.0
* @param string $more Default more string.
* @return string Modified more string.
*/
function apus_excerpt_more( $more ) {
if ( is_admin() ) {
return $more;
}
return '&hellip;';
}
add_filter( 'excerpt_more', 'apus_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 apus_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 apus_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 apus_breadcrumbs( $args = array() ) {
// Default arguments.
$defaults = array(
'separator' => '<span class="separator">/</span>',
'home_label' => esc_html__( 'Inicio', 'apus' ),
'show_home' => true,
'show_current' => true,
'before' => '<nav class="breadcrumbs" aria-label="' . esc_attr__( 'Breadcrumb', 'apus' ) . '"><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: ', 'apus' ) . 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: ', 'apus' ) . 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: ', 'apus' ) . 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', 'apus' ) . '</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 apus_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', 'apus_body_classes' );
/**
* Add custom post classes
*
* @since 1.0.0
* @param array $classes Existing post classes.
* @return array Modified post classes.
*/
function apus_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', 'apus_post_classes' );
/**
* Sanitize SVG uploads
*
* @since 1.0.0
* @param array $mimes Allowed mime types.
* @return array Modified mime types.
*/
function apus_add_svg_mime_types( $mimes ) {
$mimes['svg'] = 'image/svg+xml';
$mimes['svgz'] = 'image/svg+xml';
return $mimes;
}
add_filter( 'upload_mimes', 'apus_add_svg_mime_types' );
/**
* Pagination for archive pages
*
* @since 1.0.0
* @param array $args Pagination arguments.
*/
function apus_pagination( $args = array() ) {
global $wp_query;
// Don't print empty markup if there's only one page.
if ( $wp_query->max_num_pages < 2 ) {
return;
}
$defaults = array(
'mid_size' => 2,
'prev_text' => esc_html__( '&larr; Anterior', 'apus' ),
'next_text' => esc_html__( 'Siguiente &rarr;', 'apus' ),
'screen_reader_text' => esc_html__( 'Navegación de entradas', 'apus' ),
'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 apus_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( 'apus_footer_column_classes', $column_classes );
// Return the class for the specified column, or default to col-12 if not found.
return isset( $column_classes[ $column ] ) ? $column_classes[ $column ] : 'col-12';
}

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

@@ -0,0 +1,544 @@
<?php
/**
* Template Tags
*
* Reusable template tags for theme templates.
*
* @package Apus_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 apus_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 apus_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 apus_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'] ) {
apus_posted_on();
}
if ( $args['show_author'] ) {
apus_posted_by();
}
if ( $args['show_category'] && 'post' === get_post_type() ) {
$categories_list = get_the_category_list( esc_html__( ', ', 'apus' ) );
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>', 'apus' ),
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>', 'apus' ),
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>', 'apus' ),
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 apus_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 apus_read_more( $args = array() ) {
$defaults = array(
'text' => esc_html__( 'Leer más', 'apus' ),
'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 apus_social_share( $args = array() ) {
$defaults = array(
'title' => esc_html__( 'Compartir:', 'apus' ),
'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', 'apus' ), 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 apus_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', 'apus' ), $author_name ) )
);
}
echo '</div>';
echo '</div>';
}
/**
* Display related posts
*
* @since 1.0.0
* @param array $args Related posts arguments.
*/
function apus_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', 'apus' ),
'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 apus_table_of_contents( $args = array() ) {
global $post;
if ( ! is_single() || ! isset( $post->post_content ) ) {
return;
}
$defaults = array(
'title' => esc_html__( 'Tabla de contenidos', 'apus' ),
'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 apus_cookie_notice( $args = array() ) {
// Check if cookie consent has been given.
if ( isset( $_COOKIE['apus_cookie_consent'] ) ) {
return;
}
$defaults = array(
'message' => esc_html__( 'Este sitio utiliza cookies para mejorar su experiencia. Al continuar navegando, acepta nuestro uso de cookies.', 'apus' ),
'accept_text' => esc_html__( 'Aceptar', 'apus' ),
'learn_more' => esc_html__( 'Más información', 'apus' ),
'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 apus_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, 'apus-theme'), $reading_time);
}

View File

@@ -0,0 +1,345 @@
<?php
/**
* Theme Options Helper Functions
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Get theme option value
*
* @param string $option_name The option name
* @param mixed $default Default value if option doesn't exist
* @return mixed The option value
*/
function apus_get_option($option_name, $default = '') {
$options = get_option('apus_theme_options', array());
if (isset($options[$option_name])) {
return $options[$option_name];
}
return $default;
}
/**
* Check if option is enabled (checkbox/switch)
*
* @param string $option_name The option name
* @return bool True if enabled, false otherwise
*/
function apus_is_option_enabled($option_name) {
return (bool) apus_get_option($option_name, false);
}
/**
* Get breadcrumbs separator
*
* @return string The separator
*/
function apus_get_breadcrumb_separator() {
return apus_get_option('breadcrumb_separator', '>');
}
/**
* Check if breadcrumbs should be shown
*
* @return bool
*/
function apus_show_breadcrumbs() {
return apus_is_option_enabled('enable_breadcrumbs');
}
/**
* Get excerpt length
*
* @return int The excerpt length
*/
function apus_get_excerpt_length() {
return (int) apus_get_option('excerpt_length', 55);
}
/**
* Get excerpt more text
*
* @return string The excerpt more text
*/
function apus_get_excerpt_more() {
return apus_get_option('excerpt_more', '...');
}
/**
* Check if related posts should be shown
*
* @return bool
*/
function apus_show_related_posts() {
return apus_is_option_enabled('enable_related_posts');
}
/**
* Get number of related posts to show
*
* @return int
*/
function apus_get_related_posts_count() {
return (int) apus_get_option('related_posts_count', 3);
}
/**
* Get related posts taxonomy
*
* @return string
*/
function apus_get_related_posts_taxonomy() {
return apus_get_option('related_posts_taxonomy', 'category');
}
/**
* Get related posts title
*
* @return string
*/
function apus_get_related_posts_title() {
return apus_get_option('related_posts_title', __('Related Posts', 'apus-theme'));
}
/**
* Check if specific performance optimization is enabled
*
* @param string $optimization The optimization name
* @return bool
*/
function apus_is_performance_enabled($optimization) {
return apus_is_option_enabled('performance_' . $optimization);
}
/**
* Get copyright text
*
* @return string
*/
function apus_get_copyright_text() {
$default = sprintf(
__('&copy; %s %s. All rights reserved.', 'apus-theme'),
date('Y'),
get_bloginfo('name')
);
return apus_get_option('copyright_text', $default);
}
/**
* Get social media links
*
* @return array Array of social media links
*/
function apus_get_social_links() {
return array(
'facebook' => apus_get_option('social_facebook', ''),
'twitter' => apus_get_option('social_twitter', ''),
'instagram' => apus_get_option('social_instagram', ''),
'linkedin' => apus_get_option('social_linkedin', ''),
'youtube' => apus_get_option('social_youtube', ''),
);
}
/**
* Check if comments are enabled for posts
*
* @return bool
*/
function apus_comments_enabled_for_posts() {
return apus_is_option_enabled('enable_comments_posts');
}
/**
* Check if comments are enabled for pages
*
* @return bool
*/
function apus_comments_enabled_for_pages() {
return apus_is_option_enabled('enable_comments_pages');
}
/**
* Get default post layout
*
* @return string
*/
function apus_get_default_post_layout() {
return apus_get_option('default_post_layout', 'right-sidebar');
}
/**
* Get default page layout
*
* @return string
*/
function apus_get_default_page_layout() {
return apus_get_option('default_page_layout', 'right-sidebar');
}
/**
* Get posts per page for archive
*
* @return int
*/
function apus_get_archive_posts_per_page() {
$custom = (int) apus_get_option('archive_posts_per_page', 0);
return $custom > 0 ? $custom : get_option('posts_per_page', 10);
}
/**
* Check if featured image should be shown on single posts
*
* @return bool
*/
function apus_show_featured_image_single() {
return apus_is_option_enabled('show_featured_image_single');
}
/**
* Check if author box should be shown on single posts
*
* @return bool
*/
function apus_show_author_box() {
return apus_is_option_enabled('show_author_box');
}
/**
* Get date format
*
* @return string
*/
function apus_get_date_format() {
return apus_get_option('date_format', 'd/m/Y');
}
/**
* Get time format
*
* @return string
*/
function apus_get_time_format() {
return apus_get_option('time_format', 'H:i');
}
/**
* Get logo URL
*
* @return string
*/
function apus_get_logo_url() {
$logo_id = apus_get_option('site_logo', 0);
if ($logo_id) {
$logo = wp_get_attachment_image_url($logo_id, 'full');
if ($logo) {
return $logo;
}
}
return '';
}
/**
* Get favicon URL
*
* @return string
*/
function apus_get_favicon_url() {
$favicon_id = apus_get_option('site_favicon', 0);
if ($favicon_id) {
$favicon = wp_get_attachment_image_url($favicon_id, 'full');
if ($favicon) {
return $favicon;
}
}
return '';
}
/**
* Get custom CSS
*
* @return string
*/
function apus_get_custom_css() {
return apus_get_option('custom_css', '');
}
/**
* Get custom JS (header)
*
* @return string
*/
function apus_get_custom_js_header() {
return apus_get_option('custom_js_header', '');
}
/**
* Get custom JS (footer)
*
* @return string
*/
function apus_get_custom_js_footer() {
return apus_get_option('custom_js_footer', '');
}
/**
* Check if lazy loading is enabled
*
* @return bool
*/
function apus_is_lazy_loading_enabled() {
return apus_is_option_enabled('enable_lazy_loading');
}
/**
* Get all theme options
*
* @return array
*/
function apus_get_all_options() {
return get_option('apus_theme_options', array());
}
/**
* Reset theme options to defaults
*
* @return bool
*/
function apus_reset_options() {
return delete_option('apus_theme_options');
}
/**
* Check if Table of Contents is enabled
*
* @return bool
*/
function apus_is_toc_enabled() {
return apus_get_option('enable_toc', true);
}
/**
* Get minimum headings required to display TOC
*
* @return int
*/
function apus_get_toc_min_headings() {
return (int) apus_get_option('toc_min_headings', 2);
}
/**
* Get TOC title
*
* @return string
*/
function apus_get_toc_title() {
return apus_get_option('toc_title', __('Table of Contents', 'apus-theme'));
}

202
inc/toc.php Normal file
View File

@@ -0,0 +1,202 @@
<?php
/**
* Table of Contents (TOC) Functions
*
* This file contains functions to automatically generate a table of contents
* from post content headings (H2 and H3).
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Extract headings from post content
*
* Parses the content and extracts all H2 and H3 headings with their text.
*
* @param string $content Post content
* @return array Array of headings with level, text, and ID
*/
function apus_extract_headings($content) {
if (empty($content)) {
return array();
}
$headings = array();
// Match H2 and H3 tags with their content
preg_match_all('/<h([23])(?:[^>]*)>(.*?)<\/h\1>/i', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $index => $match) {
$level = (int) $match[1]; // 2 or 3
$text = strip_tags($match[2]); // Remove any HTML tags inside heading
// Generate a clean ID from the heading text
$id = apus_generate_heading_id($text, $index);
$headings[] = array(
'level' => $level,
'text' => $text,
'id' => $id,
);
}
return $headings;
}
/**
* Generate a clean ID for a heading
*
* Creates a URL-friendly ID from heading text.
*
* @param string $text Heading text
* @param int $index Index of the heading (for uniqueness)
* @return string Clean ID
*/
function apus_generate_heading_id($text, $index) {
// Remove special characters and convert to lowercase
$id = sanitize_title($text);
// If ID is empty, use a fallback
if (empty($id)) {
$id = 'heading-' . $index;
}
return $id;
}
/**
* Generate HTML for Table of Contents
*
* Creates a nested list structure from the headings array.
*
* @param array $headings Array of headings from apus_extract_headings()
* @return string HTML for the table of contents
*/
function apus_generate_toc($headings) {
// Get minimum headings required from theme options
$min_headings = (int) apus_get_option('toc_min_headings', 2);
if (empty($headings) || count($headings) < $min_headings) {
return ''; // Don't show TOC if there are fewer headings than required
}
// Get custom TOC title from theme options
$toc_title = apus_get_toc_title();
$toc_html = '<nav class="apus-toc" aria-label="' . esc_attr($toc_title) . '">';
$toc_html .= '<div class="apus-toc-header">';
$toc_html .= '<h2 class="apus-toc-title">' . esc_html($toc_title) . '</h2>';
$toc_html .= '<button class="apus-toc-toggle" aria-expanded="true" aria-controls="apus-toc-list">';
$toc_html .= '<span class="toggle-icon" aria-hidden="true"></span>';
$toc_html .= '<span class="screen-reader-text">' . esc_html__('Toggle Table of Contents', 'apus-theme') . '</span>';
$toc_html .= '</button>';
$toc_html .= '</div>';
$toc_html .= '<ol class="apus-toc-list" id="apus-toc-list">';
$current_level = 2;
$open_sublists = 0;
foreach ($headings as $index => $heading) {
$level = $heading['level'];
$text = esc_html($heading['text']);
$id = esc_attr($heading['id']);
// Handle level changes
if ($level > $current_level) {
// Open nested list for H3
$toc_html .= '<ol class="apus-toc-sublist">';
$open_sublists++;
} elseif ($level < $current_level && $open_sublists > 0) {
// Close nested list when going back to H2
$toc_html .= '</li></ol></li>';
$open_sublists--;
} elseif ($index > 0) {
// Close previous item
$toc_html .= '</li>';
}
$toc_html .= '<li class="apus-toc-item apus-toc-level-' . $level . '">';
$toc_html .= '<a href="#' . $id . '" class="apus-toc-link">' . $text . '</a>';
$current_level = $level;
}
// Close any open lists
$toc_html .= '</li>';
while ($open_sublists > 0) {
$toc_html .= '</ol></li>';
$open_sublists--;
}
$toc_html .= '</ol>';
$toc_html .= '</nav>';
return $toc_html;
}
/**
* Add IDs to headings in content
*
* Modifies the post content to add ID attributes to H2 and H3 headings.
*
* @param string $content Post content
* @return string Modified content with IDs added to headings
*/
function apus_add_heading_ids($content) {
if (empty($content)) {
return $content;
}
// Extract headings first to get consistent IDs
$headings = apus_extract_headings($content);
if (empty($headings)) {
return $content;
}
// Replace headings with versions that include IDs
$heading_index = 0;
$content = preg_replace_callback(
'/<h([23])(?:[^>]*)>(.*?)<\/h\1>/i',
function($matches) use ($headings, &$heading_index) {
if (!isset($headings[$heading_index])) {
return $matches[0];
}
$level = $matches[1];
$text = $matches[2];
$id = $headings[$heading_index]['id'];
$heading_index++;
return '<h' . $level . ' id="' . esc_attr($id) . '">' . $text . '</h' . $level . '>';
},
$content
);
return $content;
}
/**
* Modify post content to add heading IDs
*
* Filters the_content to add IDs to headings for TOC linking.
*
* @param string $content Post content
* @return string Modified content
*/
function apus_filter_content_add_heading_ids($content) {
// Only apply to single posts
if (!is_single()) {
return $content;
}
return apus_add_heading_ids($content);
}
add_filter('the_content', 'apus_filter_content_add_heading_ids', 10);