Agregar estructura completa del tema APUS con Bootstrap 5 y optimizaciones de rendimiento

Se implementa tema WordPress personalizado para Análisis de Precios Unitarios con funcionalidades avanzadas:
- Sistema de templates (front-page, single, archive, page, 404, search)
- Integración de Bootstrap 5.3.8 con estructura modular de assets
- Panel de opciones del tema con Customizer API
- Optimizaciones de rendimiento (Critical CSS, Image Optimization, Performance)
- Funcionalidades SEO y compatibilidad con Rank Math
- Sistema de posts relacionados y tabla de contenidos
- Badge de categorías y manejo de AdSense diferido
- Tipografías Google Fonts configurables
- Documentación completa del tema y guías de uso

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-04 09:31:47 -06:00
parent 12285bec3c
commit 7ba9080f57
67 changed files with 21825 additions and 0 deletions

View File

@@ -0,0 +1,359 @@
# Related Posts - Documentación Técnica
## Descripción General
Sistema de posts relacionados completamente configurable que muestra automáticamente posts similares al final de cada artículo individual basándose en categorías compartidas.
## Archivos Creados
### 1. `inc/related-posts.php` (294 líneas)
Archivo principal con toda la lógica de posts relacionados:
#### Funciones Principales:
- **`apus_get_related_posts($post_id)`**
- Obtiene posts relacionados por categoría
- Retorna: `WP_Query|false`
- Parámetros configurables vía `get_option()`
- Optimizado con `no_found_rows` y desactivación de cache
- **`apus_display_related_posts($post_id = null)`**
- Renderiza el HTML completo de posts relacionados
- Grid responsive con Bootstrap 5
- Soporte para posts con y sin imagen destacada
- Fondos de color para posts sin imagen
- **`apus_get_column_class($columns)`**
- Calcula clases de columna de Bootstrap según configuración
- Soporta: 1, 2, 3 o 4 columnas
- Retorna clases responsive (col-12, col-md-6, etc.)
- **`apus_hook_related_posts()`**
- Hook automático en `apus_after_post_content`
- Se ejecuta solo en single posts
- **`apus_enqueue_related_posts_styles()`**
- Carga el CSS solo cuando es necesario
- Dependencia: `apus-bootstrap`
- **`apus_related_posts_default_options()`**
- Configura opciones por defecto en la base de datos
- Se ejecuta en `after_setup_theme`
### 2. `assets/css/related-posts.css` (460 líneas)
Estilos completos para el sistema de posts relacionados:
#### Secciones Principales:
- **Related Posts Section**: Contenedor principal y título con línea decorativa
- **Card Base**: Estructura de tarjetas con sombras y transiciones
- **Card with Thumbnail**: Tarjetas con imagen destacada (aspect ratio 4:3)
- **Card without Image**: Tarjetas con fondo de color y título centrado
- **Category Badge**: Badge flotante con backdrop-filter
- **Card Content**: Título, excerpt y metadata
- **Responsive**: Breakpoints para tablet, mobile y small mobile
- **Print Styles**: Optimización para impresión
- **Dark Mode**: Soporte para `prefers-color-scheme: dark`
- **Accessibility**: Focus states y reduced motion
### 3. `inc/admin/related-posts-options.php` (220+ líneas)
Helpers y documentación para configuración:
- **`apus_get_related_posts_options()`**: Obtiene todas las opciones disponibles
- **`apus_update_related_posts_option()`**: Actualiza una opción específica
- **`apus_reset_related_posts_options()`**: Resetea a valores por defecto
- **`apus_get_related_posts_documentation()`**: Documentación estructurada
## Opciones Configurables
### Opciones Disponibles (WordPress Options API)
```php
// Habilitar/deshabilitar
'apus_related_posts_enabled' => true|false (default: true)
// Título de la sección
'apus_related_posts_title' => string (default: 'Related Posts')
// Cantidad de posts
'apus_related_posts_count' => int (default: 3, min: 1, max: 12)
// Columnas en el grid
'apus_related_posts_columns' => int (default: 3, options: 1-4)
// Mostrar excerpt
'apus_related_posts_show_excerpt' => true|false (default: true)
// Longitud del excerpt
'apus_related_posts_excerpt_length' => int (default: 20, min: 5, max: 100)
// Mostrar fecha
'apus_related_posts_show_date' => true|false (default: true)
// Mostrar categoría
'apus_related_posts_show_category' => true|false (default: true)
// Colores de fondo para posts sin imagen
'apus_related_posts_bg_colors' => array (default: 6 colores)
```
### Ejemplos de Configuración
#### Ejemplo 1: Cambiar título y cantidad
```php
update_option('apus_related_posts_title', 'También te puede interesar');
update_option('apus_related_posts_count', 4);
```
#### Ejemplo 2: Layout de 2 columnas sin excerpt
```php
update_option('apus_related_posts_columns', 2);
update_option('apus_related_posts_show_excerpt', false);
```
#### Ejemplo 3: Colores personalizados
```php
update_option('apus_related_posts_bg_colors', array(
'#FF6B6B', // Rojo
'#4ECDC4', // Teal
'#45B7D1', // Azul
'#FFA07A', // Coral
'#98D8C8', // Menta
'#F7DC6F', // Amarillo
));
```
#### Ejemplo 4: Deshabilitar temporalmente
```php
update_option('apus_related_posts_enabled', false);
```
## Hooks y Filtros
### Filter: `apus_related_posts_args`
Modifica los argumentos de WP_Query para posts relacionados:
```php
add_filter('apus_related_posts_args', function($args, $post_id) {
// Ordenar por fecha en vez de aleatorio
$args['orderby'] = 'date';
$args['order'] = 'DESC';
// Solo posts de los últimos 6 meses
$args['date_query'] = array(
array('after' => '6 months ago')
);
// Excluir categoría específica
$args['category__not_in'] = array(5);
return $args;
}, 10, 2);
```
### Action: `apus_after_post_content`
Hook donde se renderiza el contenido relacionado (ya implementado en `single.php`):
```php
// En single.php (línea 210)
do_action('apus_after_post_content');
```
## Características Técnicas
### Performance
- ✅ Query optimizado con `no_found_rows`
- ✅ Cache desactivado para posts relacionados
- ✅ CSS cargado solo en single posts
- ✅ Lazy loading de imágenes
- ✅ Aspect ratio CSS nativo
### Responsive Design
- ✅ Bootstrap 5 grid system
- ✅ Breakpoints: mobile (575px), tablet (768px), desktop (992px)
- ✅ Aspect ratio adaptativo (60% mobile, 75% desktop)
- ✅ Tamaños de fuente escalables
### Accesibilidad
- ✅ Focus states visibles
-`prefers-reduced-motion` support
- ✅ Semantic HTML
- ✅ Alt text en imágenes
- ✅ Contraste de colores adecuado
### SEO
- ✅ Estructura semántica con `<article>` y `<section>`
- ✅ Uso de `<time>` con datetime
- ✅ Enlaces con rel attributes
- ✅ Metadata estructurada
### Print Styles
- ✅ Bordes en vez de sombras
- ✅ Oculta imágenes para ahorrar tinta
- ✅ Optimización de espacios
- ✅ Evita page breaks dentro de cards
## Integración en el Tema
### En `functions.php`
```php
// Líneas 189-192
if (file_exists(get_template_directory() . '/inc/related-posts.php')) {
require_once get_template_directory() . '/inc/related-posts.php';
}
```
### En `single.php`
```php
// Línea 210 - Hook existente
do_action('apus_after_post_content');
```
### Enqueue de Estilos
El CSS se carga automáticamente via `wp_enqueue_scripts` solo en single posts:
```php
wp_enqueue_style(
'apus-related-posts',
get_template_directory_uri() . '/assets/css/related-posts.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
```
## Comportamiento de Posts sin Imagen
Cuando un post relacionado no tiene imagen destacada:
1. **Se genera un fondo de color** usando la paleta configurada
2. **El título se muestra centrado** sobre el fondo de color
3. **Se rota el color** usando módulo sobre el array de colores
4. **Category badge** tiene estilo especial con backdrop-filter
### Ejemplo Visual
```
┌─────────────────────────┐
│ ╔═══════════════╗ │
│ ║ [Categoría] ║ │
│ ╚═══════════════╝ │
│ │
│ Título del Post │
│ Relacionado Aquí │
│ │
└─────────────────────────┘
(Fondo de color sólido)
```
## Testing
### Casos de Prueba Recomendados
1. **Post con 3+ posts relacionados**: Verificar grid y layout
2. **Post con 1-2 posts relacionados**: Verificar responsive
3. **Post sin posts relacionados**: No debe mostrar sección
4. **Posts sin categorías**: No debe mostrar sección
5. **Mix de posts con/sin imagen**: Verificar colores rotatorios
6. **Diferentes configuraciones de columnas**: 1, 2, 3, 4
7. **Diferentes viewport sizes**: Mobile, tablet, desktop
8. **Print preview**: Verificar estilos de impresión
9. **Dark mode**: Si el navegador lo soporta
10. **Reduced motion**: Verificar que no haya animaciones
### Comandos de Verificación
```bash
# Verificar archivos creados
ls -la wp-content/themes/apus-theme/inc/related-posts.php
ls -la wp-content/themes/apus-theme/assets/css/related-posts.css
ls -la wp-content/themes/apus-theme/inc/admin/related-posts-options.php
# Contar líneas
wc -l wp-content/themes/apus-theme/inc/related-posts.php
wc -l wp-content/themes/apus-theme/assets/css/related-posts.css
# Verificar funciones
grep "^function apus_" wp-content/themes/apus-theme/inc/related-posts.php
# Verificar inclusión en functions.php
grep "related-posts" wp-content/themes/apus-theme/functions.php
```
## Troubleshooting
### Los posts relacionados no aparecen
1. **Verificar que está habilitado**:
```php
var_dump(get_option('apus_related_posts_enabled'));
```
2. **Verificar que el post tiene categorías**:
```php
var_dump(wp_get_post_categories(get_the_ID()));
```
3. **Verificar que hay posts en la misma categoría**:
```php
$query = apus_get_related_posts(get_the_ID());
var_dump($query ? $query->post_count : 'No query');
```
### El CSS no se carga
1. **Verificar ruta del archivo**:
```php
var_dump(file_exists(get_template_directory() . '/assets/css/related-posts.css'));
```
2. **Limpiar cache del navegador**
3. **Verificar que estás en single post**:
```php
var_dump(is_single() && !is_attachment());
```
### Los colores no cambian
1. **Verificar array de colores**:
```php
var_dump(get_option('apus_related_posts_bg_colors'));
```
2. **Verificar que hay posts sin imagen** en los relacionados
## Compatibilidad
- ✅ WordPress 5.0+
- ✅ PHP 7.4+
- ✅ Bootstrap 5.3.8
- ✅ Navegadores modernos (últimas 2 versiones)
- ✅ IE11 con degradación graceful
## Mantenimiento Futuro
### Posibles Mejoras
1. **Panel de administración**: Interfaz visual en el admin de WordPress
2. **Más criterios de relación**: Tags, autor, custom taxonomies
3. **Cache inteligente**: Transients API para queries
4. **Shortcode**: Para mostrar relacionados en cualquier lugar
5. **Widget**: Para sidebar
6. **AJAX loading**: Carga asíncrona de posts
7. **Infinite scroll**: Para móvil
8. **A/B testing**: Diferentes layouts
### Consideraciones de Actualización
- Mantener retrocompatibilidad con opciones existentes
- Documentar cambios en CHANGELOG
- Probar con diferentes configuraciones
- Verificar performance en sitios grandes
## Issue Relacionado
**Issue #13**: Posts relacionados configurables - ✅ COMPLETADO
**Fecha de Implementación**: 2025-11-03
**Desarrollador**: Claude Code (Anthropic)

View File

@@ -0,0 +1,227 @@
# Apus Theme Options Panel
## Overview
Complete theme options panel for managing all theme settings from WordPress admin.
## Location
`Appearance > Theme Options` in WordPress admin
## Files Structure
```
inc/admin/
├── theme-options.php # Main admin page registration
├── options-api.php # Settings API and sanitization
├── options-page-template.php # HTML template for options page
└── README.md # This file
inc/
└── theme-options-helpers.php # Helper functions to get options
assets/admin/
├── css/
│ └── theme-options.css # Admin styles
└── js/
└── theme-options.js # Admin JavaScript
```
## Features
### General Tab
- Site logo upload
- Site favicon upload
- Breadcrumbs enable/disable
- Breadcrumb separator customization
- Date and time format
- Copyright text
- Social media links (Facebook, Twitter, Instagram, LinkedIn, YouTube)
### Content Tab
- Excerpt length
- Excerpt more text
- Default post/page layouts
- Archive posts per page
- Featured image display
- Author box display
- Comments enable/disable for posts/pages
- Post meta visibility
- Tags and categories display
### Performance Tab
- Lazy loading
- Remove emoji scripts
- Remove embeds
- Remove Dashicons on frontend
- Defer JavaScript
- Minify HTML
- Disable Gutenberg
### Related Posts Tab
- Enable/disable related posts
- Number of posts to show
- Taxonomy to use (category, tag, or both)
- Section title
- Number of columns
### Advanced Tab
- Custom CSS
- Custom JavaScript (header)
- Custom JavaScript (footer)
## Usage
### Getting Options in Templates
```php
// Get any option with default fallback
$value = apus_get_option('option_name', 'default_value');
// Check if option is enabled (boolean)
if (apus_is_option_enabled('enable_breadcrumbs')) {
// Do something
}
// Specific helper functions
$logo_url = apus_get_logo_url();
$excerpt_length = apus_get_excerpt_length();
$social_links = apus_get_social_links();
```
### Available Helper Functions
All helper functions are in `inc/theme-options-helpers.php`:
- `apus_get_option($option_name, $default)`
- `apus_is_option_enabled($option_name)`
- `apus_get_breadcrumb_separator()`
- `apus_show_breadcrumbs()`
- `apus_get_excerpt_length()`
- `apus_get_excerpt_more()`
- `apus_show_related_posts()`
- `apus_get_related_posts_count()`
- `apus_get_related_posts_taxonomy()`
- `apus_get_related_posts_title()`
- `apus_is_performance_enabled($optimization)`
- `apus_get_copyright_text()`
- `apus_get_social_links()`
- `apus_comments_enabled_for_posts()`
- `apus_comments_enabled_for_pages()`
- `apus_get_default_post_layout()`
- `apus_get_default_page_layout()`
- `apus_get_archive_posts_per_page()`
- `apus_show_featured_image_single()`
- `apus_show_author_box()`
- `apus_get_date_format()`
- `apus_get_time_format()`
- `apus_get_logo_url()`
- `apus_get_favicon_url()`
- `apus_get_custom_css()`
- `apus_get_custom_js_header()`
- `apus_get_custom_js_footer()`
- `apus_is_lazy_loading_enabled()`
- `apus_get_all_options()`
- `apus_reset_options()`
## Import/Export
### Export Options
1. Go to `Appearance > Theme Options`
2. Click "Export Options" button
3. A JSON file will be downloaded with all current settings
### Import Options
1. Go to `Appearance > Theme Options`
2. Click "Import Options" button
3. Paste the JSON content from your exported file
4. Click "Import"
5. Page will reload with imported settings
## Reset to Defaults
Click "Reset to Defaults" button to restore all options to their default values. This action requires confirmation.
## Sanitization
All options are sanitized before saving:
- Text fields: `sanitize_text_field()`
- URLs: `esc_url_raw()`
- HTML content: `wp_kses_post()`
- Integers: `absint()`
- Checkboxes: Boolean conversion
- CSS: Custom sanitization removing scripts
- JavaScript: Custom sanitization removing PHP code
## Hooks Available
### Actions
- `apus_before_options_save` - Before options are saved
- `apus_after_options_save` - After options are saved
### Filters
- `apus_theme_options` - Filter all options
- `apus_default_options` - Filter default options
## JavaScript Events
Custom events triggered by the options panel:
- `apus:options:saved` - When options are saved
- `apus:options:reset` - When options are reset
- `apus:options:imported` - When options are imported
- `apus:options:exported` - When options are exported
## Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
## Accessibility
- Keyboard navigation supported
- Screen reader friendly
- WCAG 2.1 Level AA compliant
- Focus indicators visible
## Security
- Nonce verification on all AJAX calls
- Capability checks (`manage_options`)
- Input sanitization
- Output escaping
- CSRF protection
## Performance
- Lazy loading for tab content
- Conditional script loading (only on options page)
- Optimized AJAX requests
- Minimal DOM manipulation
## Troubleshooting
### Options not saving
1. Check WordPress user has `manage_options` capability
2. Check file permissions
3. Check for JavaScript errors in browser console
4. Verify WordPress nonce is valid
### Images not uploading
1. Check PHP upload_max_filesize setting
2. Check WordPress media upload permissions
3. Check browser console for errors
### Import not working
1. Verify JSON format is valid
2. Check for special characters in JSON
3. Ensure JSON is from same theme version
## Version History
### 1.0.0
- Initial release
- All basic options implemented
- Import/Export functionality
- Reset to defaults
- Full sanitization

View File

@@ -0,0 +1,360 @@
# Theme Options Testing Checklist
## Pre-Testing Setup
- [ ] Activate the Apus Theme
- [ ] Verify you're logged in as an administrator
- [ ] Check PHP error_log for any warnings/errors
- [ ] Open browser console (F12) to check for JavaScript errors
## Admin Panel Access
- [ ] Navigate to `Appearance > Theme Options` in WordPress admin
- [ ] Verify the page loads without errors
- [ ] Check that the page title shows "Apus Theme Options"
- [ ] Verify the version number is displayed (v1.0.0)
- [ ] Confirm all 5 tabs are visible (General, Content, Performance, Related Posts, Advanced)
## General Tab Testing
### Logo Upload
- [ ] Click "Upload Logo" button
- [ ] Verify WordPress media library opens
- [ ] Upload an image (recommended: 200x60px)
- [ ] Verify image preview appears
- [ ] Click "Remove Logo" button
- [ ] Verify preview disappears and hidden input is cleared
- [ ] Re-upload logo for further testing
### Favicon Upload
- [ ] Click "Upload Favicon" button
- [ ] Upload a favicon image (recommended: 32x32px)
- [ ] Verify preview appears
- [ ] Test remove functionality
### Breadcrumbs
- [ ] Toggle breadcrumbs enable/disable switch
- [ ] Verify the switch animation works
- [ ] Change breadcrumb separator (try: >, /, », →)
- [ ] Save settings
### Date/Time Format
- [ ] Change date format (try: d/m/Y, m/d/Y, Y-m-d)
- [ ] Change time format (try: H:i, g:i A)
- [ ] Save settings
### Copyright Text
- [ ] Edit copyright text
- [ ] Try adding HTML (like `<strong>`, `<a>`)
- [ ] Save and verify HTML is preserved (not stripped)
### Social Media Links
- [ ] Add Facebook URL
- [ ] Add Twitter URL
- [ ] Add Instagram URL
- [ ] Add LinkedIn URL
- [ ] Add YouTube URL
- [ ] Try invalid URLs (should show validation error)
- [ ] Save valid URLs
## Content Tab Testing
### Excerpt Settings
- [ ] Change excerpt length (try: 30, 55, 100)
- [ ] Change excerpt more text (try: ..., [Read more], →)
- [ ] Save settings
### Layout Settings
- [ ] Change default post layout (Right Sidebar, Left Sidebar, No Sidebar)
- [ ] Change default page layout
- [ ] Save settings
### Archive Settings
- [ ] Change archive posts per page (try: 10, 20, 0)
- [ ] Save settings
### Display Options
- [ ] Toggle "Show Featured Image" on single posts
- [ ] Toggle "Show Author Box"
- [ ] Toggle "Enable Comments on Posts"
- [ ] Toggle "Enable Comments on Pages"
- [ ] Toggle "Show Post Meta"
- [ ] Toggle "Show Post Tags"
- [ ] Toggle "Show Post Categories"
- [ ] Save settings
## Performance Tab Testing
### Performance Options
- [ ] Toggle "Enable Lazy Loading"
- [ ] Toggle "Remove Emoji Scripts"
- [ ] Toggle "Remove Embeds"
- [ ] Toggle "Remove Dashicons"
- [ ] Toggle "Defer JavaScript"
- [ ] Toggle "Minify HTML"
- [ ] Toggle "Disable Gutenberg"
- [ ] Save settings
- [ ] Verify front-end reflects changes (check page source)
## Related Posts Tab Testing
### Related Posts Configuration
- [ ] Toggle "Enable Related Posts"
- [ ] Verify that when disabled, other fields become disabled/grayed out
- [ ] Enable related posts
- [ ] Change number of related posts (try: 1, 3, 6, 12)
- [ ] Change taxonomy (Category, Tag, Both)
- [ ] Change related posts title
- [ ] Change columns (2, 3, 4)
- [ ] Save settings
## Advanced Tab Testing
### Custom CSS
- [ ] Add custom CSS code (e.g., `body { background: #f0f0f0; }`)
- [ ] Try adding `<script>` tags (should be removed on save)
- [ ] Save settings
- [ ] Check front-end page source for custom CSS in `<head>`
### Custom JavaScript
- [ ] Add custom JS in header (e.g., `console.log('Header JS');`)
- [ ] Add custom JS in footer (e.g., `console.log('Footer JS');`)
- [ ] Try adding `<?php` tags (should be removed)
- [ ] Save settings
- [ ] Check front-end page source for scripts
- [ ] Check browser console for log messages
## Form Validation Testing
### Required Fields
- [ ] Try saving with empty required fields
- [ ] Verify error highlighting appears
- [ ] Verify scroll to first error works
### Number Fields
- [ ] Try entering values below minimum
- [ ] Try entering values above maximum
- [ ] Try entering negative numbers where not allowed
- [ ] Verify validation works
### URL Fields
- [ ] Try invalid URLs (e.g., "not a url")
- [ ] Verify validation shows error
- [ ] Enter valid URLs
- [ ] Save successfully
## Import/Export Testing
### Export
- [ ] Click "Export Options" button
- [ ] Verify JSON file downloads
- [ ] Open file and verify it contains valid JSON
- [ ] Verify all settings are in the export
### Import
- [ ] Make some changes to settings
- [ ] Click "Import Options" button
- [ ] Verify modal opens
- [ ] Paste invalid JSON (should show error)
- [ ] Paste valid JSON from export
- [ ] Click "Import" button
- [ ] Verify success message appears
- [ ] Verify page reloads
- [ ] Confirm settings are restored
## Reset to Defaults Testing
- [ ] Make changes to various settings
- [ ] Click "Reset to Defaults" button
- [ ] Verify confirmation dialog appears
- [ ] Cancel the dialog (settings should remain)
- [ ] Click "Reset to Defaults" again
- [ ] Confirm the reset
- [ ] Verify success message
- [ ] Verify page reloads
- [ ] Confirm all settings are back to defaults
## Tab Navigation Testing
### Tab Switching
- [ ] Click each tab and verify content switches
- [ ] Verify active tab styling is correct
- [ ] Check URL hash changes (e.g., #general, #content)
- [ ] Refresh page with hash in URL
- [ ] Verify correct tab loads on page load
- [ ] Use browser back/forward buttons
- [ ] Verify tabs respond to navigation
## Save Settings Testing
- [ ] Make changes in multiple tabs
- [ ] Click "Save All Settings" button
- [ ] Verify success message appears
- [ ] Refresh page
- [ ] Verify all changes persisted
- [ ] Check database (`wp_options` table for `apus_theme_options`)
## Helper Functions Testing
Create a test page template and test each helper function:
```php
// Test in a template file
<?php
// Test logo
$logo = apus_get_logo_url();
echo $logo ? 'Logo URL: ' . $logo : 'No logo set';
// Test breadcrumbs
echo 'Breadcrumbs enabled: ' . (apus_show_breadcrumbs() ? 'Yes' : 'No');
// Test excerpt
echo 'Excerpt length: ' . apus_get_excerpt_length();
// Test related posts
echo 'Related posts enabled: ' . (apus_show_related_posts() ? 'Yes' : 'No');
// Test social links
$social = apus_get_social_links();
print_r($social);
// Test all options
$all = apus_get_all_options();
print_r($all);
?>
```
- [ ] Test each helper function returns expected values
- [ ] Test default values when options not set
- [ ] Test boolean helper functions return true/false
- [ ] Test get_option with custom defaults
## Front-End Integration Testing
### Logo Display
- [ ] Visit front-end site
- [ ] Verify logo appears in header (if set)
- [ ] Verify favicon appears in browser tab
### Breadcrumbs
- [ ] Visit a single post
- [ ] Verify breadcrumbs appear (if enabled)
- [ ] Check separator is correct
- [ ] Visit a category archive
- [ ] Verify breadcrumbs work correctly
### Related Posts
- [ ] Visit a single post
- [ ] Scroll to bottom
- [ ] Verify related posts appear (if enabled)
- [ ] Check count matches settings
- [ ] Verify title matches settings
- [ ] Check layout columns are correct
### Comments
- [ ] Visit a post (verify comments shown/hidden based on settings)
- [ ] Visit a page (verify comments shown/hidden based on settings)
### Performance
- [ ] Check page source for removed scripts (emoji, embeds, dashicons)
- [ ] Check if lazy loading is applied to images
- [ ] Check if custom CSS appears in head
- [ ] Check if custom JS appears in head/footer
### Social Links
- [ ] Check footer for social links
- [ ] Verify all entered links appear
- [ ] Test links open in new tab
### Copyright
- [ ] Check footer for copyright text
- [ ] Verify HTML formatting is preserved
## Responsive Testing
- [ ] Test options page on desktop (1920px)
- [ ] Test on tablet (768px)
- [ ] Test on mobile (375px)
- [ ] Verify tabs switch to mobile layout
- [ ] Verify forms remain usable
- [ ] Test all buttons work on mobile
## Browser Compatibility Testing
- [ ] Test in Chrome
- [ ] Test in Firefox
- [ ] Test in Safari
- [ ] Test in Edge
- [ ] Verify all features work in each browser
## Accessibility Testing
- [ ] Test keyboard navigation (Tab, Enter, Escape)
- [ ] Test with screen reader (NVDA, JAWS, or VoiceOver)
- [ ] Verify focus indicators are visible
- [ ] Check color contrast meets WCAG standards
- [ ] Verify all images have alt text
- [ ] Check form labels are properly associated
## Security Testing
- [ ] Verify nonces are checked on all AJAX calls
- [ ] Test capability checks (log out and try accessing page)
- [ ] Try injecting `<script>` tags in text fields
- [ ] Try injecting SQL in fields
- [ ] Try injecting PHP code in custom CSS/JS
- [ ] Verify all outputs are escaped
- [ ] Check CSRF protection works
## Performance Testing
- [ ] Check page load time of options page
- [ ] Verify no memory leaks in browser
- [ ] Check network tab for unnecessary requests
- [ ] Verify scripts/styles only load on options page
- [ ] Test with large amounts of data in textareas
## Error Handling Testing
- [ ] Disconnect from internet and try saving (should show error)
- [ ] Modify nonce and try saving (should fail)
- [ ] Try uploading very large image (should handle gracefully)
- [ ] Try importing corrupted JSON (should show error)
- [ ] Fill textarea with 100,000 characters (should save)
## Console Testing
Throughout all testing, monitor for:
- [ ] JavaScript errors in console
- [ ] PHP errors in server logs
- [ ] WordPress debug.log errors
- [ ] Network errors in Network tab
- [ ] Deprecation warnings
## Final Verification
- [ ] All settings save correctly
- [ ] All settings load correctly on page refresh
- [ ] Front-end reflects all settings changes
- [ ] No JavaScript errors anywhere
- [ ] No PHP errors/warnings
- [ ] No console errors
- [ ] Page performance is acceptable
- [ ] Mobile experience is good
- [ ] Accessibility is maintained
## Sign-Off
Tested by: _______________
Date: _______________
Version: 1.0.0
Browser(s): _______________
WordPress Version: _______________
PHP Version: _______________
All tests passed: [ ] Yes [ ] No
Issues found (if any):
______________________________
______________________________
______________________________

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
}
}

View File

@@ -0,0 +1,282 @@
<?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'
);
}
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,
// 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>';
}
/**
* 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;
// 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;
}
/**
* Sanitize CSS
*
* @param string $css The CSS string
* @return string The sanitized CSS
*/
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);
}
/**
* Sanitize JavaScript
*
* @param string $js The JavaScript string
* @return string The sanitized JavaScript
*/
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);
}
/**
* Sanitize checkbox input
*
* @param mixed $input The input value
* @return bool
*/
function apus_sanitize_checkbox($input) {
return (bool) $input;
}
/**
* Sanitize integer input
*
* @param mixed $input The input value
* @return int
*/
function apus_sanitize_integer($input) {
return absint($input);
}
/**
* Sanitize text field
*
* @param string $input The input value
* @return string
*/
function apus_sanitize_text($input) {
return sanitize_text_field($input);
}
/**
* Sanitize URL
*
* @param string $input The input value
* @return string
*/
function apus_sanitize_url($input) {
return esc_url_raw($input);
}
/**
* Sanitize HTML content
*
* @param string $input The input value
* @return string
*/
function apus_sanitize_html($input) {
return wp_kses_post($input);
}

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));",
),
),
),
);
}

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');

View File

@@ -0,0 +1,163 @@
<?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;
}
/**
* Delay AdSense scripts by intercepting output buffer
*
* This function starts output buffering and replaces AdSense scripts
* with delayed versions when the page is rendered.
*/
function apus_delay_adsense_scripts() {
// Only run on frontend
if (is_admin()) {
return;
}
// Check if AdSense delay is enabled in theme options
$delay_enabled = get_theme_mod('apus_adsense_delay_enabled', true);
if (!$delay_enabled) {
return;
}
// Start output buffering
ob_start('apus_replace_adsense_scripts');
}
add_action('template_redirect', 'apus_delay_adsense_scripts', 1);
/**
* Replace AdSense scripts with delayed versions
*
* This function processes the HTML output and replaces standard AdSense
* script tags with delayed loading versions.
*
* @param string $html The HTML content to process
* @return string Modified HTML with delayed AdSense scripts
*/
function apus_replace_adsense_scripts($html) {
// Only process if there's actual AdSense content
if (strpos($html, 'pagead2.googlesyndication.com') === false &&
strpos($html, 'adsbygoogle.js') === false) {
return $html;
}
// Pattern to match AdSense script tags
$patterns = array(
// Match async script tags for AdSense
'/<script\s+async\s+src=["\']https:\/\/pagead2\.googlesyndication\.com\/pagead\/js\/adsbygoogle\.js[^"\']*["\']\s*(?:crossorigin=["\']anonymous["\'])?\s*><\/script>/i',
// Match script tags without async
'/<script\s+src=["\']https:\/\/pagead2\.googlesyndication\.com\/pagead\/js\/adsbygoogle\.js[^"\']*["\']\s*(?:crossorigin=["\']anonymous["\'])?\s*><\/script>/i',
// Match inline adsbygoogle.push scripts
'/<script>\s*\(adsbygoogle\s*=\s*window\.adsbygoogle\s*\|\|\s*\[\]\)\.push\(\{[^}]*\}\);\s*<\/script>/is',
);
// Replace async AdSense scripts with delayed versions
$replacements = array(
// Replace async script tag with data attribute for delayed loading
'<script type="text/plain" data-adsense-script src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" crossorigin="anonymous"></script>',
// Replace non-async script tag
'<script type="text/plain" data-adsense-script src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" crossorigin="anonymous"></script>',
// Replace inline push scripts with delayed versions
'<script type="text/plain" data-adsense-push>$0</script>',
);
// First pass: replace script tags
$html = preg_replace($patterns[0], $replacements[0], $html);
$html = preg_replace($patterns[1], $replacements[1], $html);
// Second pass: replace inline push calls
$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
);
// Add a comment to indicate processing occurred
if (defined('WP_DEBUG') && WP_DEBUG) {
$html = str_replace('</body>', '<!-- AdSense scripts delayed by Apus Theme --></body>', $html);
}
return $html;
}
/**
* Add inline script to initialize delayed AdSense
*
* This adds a small inline script that marks AdSense as ready to load
* after the adsense-loader.js has been enqueued.
*/
function apus_add_adsense_init_script() {
$delay_enabled = get_theme_mod('apus_adsense_delay_enabled', true);
if (!$delay_enabled || is_admin()) {
return;
}
?>
<script>
// Initialize AdSense delay flag
window.apusAdsenseDelayed = true;
</script>
<?php
}
add_action('wp_head', 'apus_add_adsense_init_script', 1);
/**
* Register customizer settings for AdSense delay
*
* @param WP_Customize_Manager $wp_customize Theme Customizer object
*/
function apus_adsense_delay_customizer($wp_customize) {
// Add Performance section if it doesn't exist
if (!$wp_customize->get_section('apus_performance')) {
$wp_customize->add_section('apus_performance', array(
'title' => __('Performance Settings', 'apus-theme'),
'priority' => 130,
));
}
// Add setting for AdSense delay
$wp_customize->add_setting('apus_adsense_delay_enabled', array(
'default' => true,
'sanitize_callback' => 'apus_sanitize_checkbox',
'transport' => 'refresh',
));
// Add control for AdSense delay
$wp_customize->add_control('apus_adsense_delay_enabled', array(
'label' => __('Delay AdSense Loading', 'apus-theme'),
'description' => __('Delay AdSense scripts until user interaction to improve page load speed.', 'apus-theme'),
'section' => 'apus_performance',
'type' => 'checkbox',
));
}
add_action('customize_register', 'apus_adsense_delay_customizer');
/**
* Sanitize checkbox input
*
* @param bool $checked Whether the checkbox is checked
* @return bool Sanitized value
*/
function apus_sanitize_checkbox($checked) {
return (isset($checked) && $checked === true || $checked === '1') ? true : false;
}

View File

@@ -0,0 +1,208 @@
<?php
/**
* Category Badge Functions
*
* Display category badges for posts with configurable settings.
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Get Category Badge HTML
*
* Returns HTML markup for the first category badge.
* Can be configured to show/hide via theme customizer.
*
* @param int $post_id Optional. Post ID. Defaults to current post.
* @param bool $force_show Optional. Force display regardless of settings. Default false.
* @return string HTML markup for category badge or empty string.
*/
function apus_get_category_badge($post_id = null, $force_show = false) {
// Get post ID if not provided
if (!$post_id) {
$post_id = get_the_ID();
}
// Check if category badges are enabled
if (!$force_show && !apus_is_category_badge_enabled()) {
return '';
}
// Get categories
$categories = get_the_category($post_id);
// Return empty if no categories
if (empty($categories)) {
return '';
}
// Get first category
$category = $categories[0];
// Build badge HTML
$output = '<div class="category-badge-wrapper">';
$output .= sprintf(
'<a href="%s" class="category-badge" rel="category tag">%s</a>',
esc_url(get_category_link($category->term_id)),
esc_html($category->name)
);
$output .= '</div>';
return $output;
}
/**
* Display Category Badge
*
* Echoes the category badge HTML.
*
* @param int $post_id Optional. Post ID. Defaults to current post.
* @param bool $force_show Optional. Force display regardless of settings. Default false.
*/
function apus_the_category_badge($post_id = null, $force_show = false) {
echo apus_get_category_badge($post_id, $force_show);
}
/**
* Check if Category Badge is Enabled
*
* @return bool True if enabled, false otherwise.
*/
function apus_is_category_badge_enabled() {
return (bool) get_theme_mod('apus_category_badge_enabled', true);
}
/**
* Register Category Badge Settings in Customizer
*
* Adds controls to enable/disable category badges.
*
* @param WP_Customize_Manager $wp_customize Theme Customizer object.
*/
function apus_category_badge_customizer($wp_customize) {
// Add section
$wp_customize->add_section('apus_category_badge', array(
'title' => __('Category Badge', 'apus-theme'),
'priority' => 31,
));
// Enable/Disable setting
$wp_customize->add_setting('apus_category_badge_enabled', array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
'transport' => 'refresh',
));
$wp_customize->add_control('apus_category_badge_enabled', array(
'label' => __('Enable Category Badge', 'apus-theme'),
'description' => __('Show the first category as a badge above the post title in single posts.', 'apus-theme'),
'section' => 'apus_category_badge',
'type' => 'checkbox',
));
// Badge background color
$wp_customize->add_setting('apus_category_badge_bg_color', array(
'default' => '#0073aa',
'sanitize_callback' => 'sanitize_hex_color',
'transport' => 'postMessage',
));
$wp_customize->add_control(new WP_Customize_Color_Control($wp_customize, 'apus_category_badge_bg_color', array(
'label' => __('Badge Background Color', 'apus-theme'),
'section' => 'apus_category_badge',
)));
// Badge text color
$wp_customize->add_setting('apus_category_badge_text_color', array(
'default' => '#ffffff',
'sanitize_callback' => 'sanitize_hex_color',
'transport' => 'postMessage',
));
$wp_customize->add_control(new WP_Customize_Color_Control($wp_customize, 'apus_category_badge_text_color', array(
'label' => __('Badge Text Color', 'apus-theme'),
'section' => 'apus_category_badge',
)));
}
add_action('customize_register', 'apus_category_badge_customizer');
/**
* Output Category Badge Inline Styles
*
* Outputs custom CSS for category badge colors.
*/
function apus_category_badge_styles() {
$bg_color = get_theme_mod('apus_category_badge_bg_color', '#0073aa');
$text_color = get_theme_mod('apus_category_badge_text_color', '#ffffff');
$css = "
<style id='apus-category-badge-inline-css'>
.category-badge-wrapper {
margin-bottom: 1rem;
}
.category-badge {
display: inline-block;
background-color: {$bg_color};
color: {$text_color};
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.05em;
border-radius: 4px;
transition: opacity 0.2s ease;
}
.category-badge:hover {
opacity: 0.8;
color: {$text_color};
}
.category-badge:focus {
outline: 2px solid {$bg_color};
outline-offset: 2px;
}
</style>
";
echo $css;
}
add_action('wp_head', 'apus_category_badge_styles');
/**
* Customize Preview JS for Live Preview
*
* Adds live preview support for category badge color changes.
*/
function apus_category_badge_customize_preview_js() {
?>
<script type="text/javascript">
(function($) {
// Background Color
wp.customize('apus_category_badge_bg_color', function(value) {
value.bind(function(newval) {
$('.category-badge').css('background-color', newval);
});
});
// Text Color
wp.customize('apus_category_badge_text_color', function(value) {
value.bind(function(newval) {
$('.category-badge').css('color', newval);
});
});
})(jQuery);
</script>
<?php
}
add_action('customize_preview_init', function() {
add_action('wp_footer', 'apus_category_badge_customize_preview_js');
});

View File

@@ -0,0 +1,378 @@
<?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' );
/**
* Sanitize checkbox value
*
* @since 1.0.0
* @param bool $checked Whether the checkbox is checked.
* @return bool
*/
function apus_sanitize_checkbox( $checked ) {
return ( isset( $checked ) && true === $checked ) ? true : false;
}
/**
* 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' );

View File

@@ -0,0 +1,160 @@
<?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');
/**
* Sanitize checkbox
*/
function apus_sanitize_checkbox($input) {
return (isset($input) && true === $input) ? true : false;
}
/**
* Sanitize select
*/
function apus_sanitize_select($input, $setting) {
$choices = $setting->manager->get_control($setting->id)->choices;
return (array_key_exists($input, $choices) ? $input : $setting->default);
}
/**
* 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);

View File

@@ -0,0 +1,233 @@
<?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() {
wp_enqueue_style(
'apus-fonts',
get_template_directory_uri() . '/assets/css/fonts.css',
array(),
'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/css/bootstrap.min.css',
array('apus-fonts'),
'5.3.8',
'all'
);
// Bootstrap JS Bundle - in footer with defer
wp_enqueue_script(
'apus-bootstrap-js',
get_template_directory_uri() . '/assets/js/bootstrap.bundle.min.js',
array(),
'5.3.8',
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 header styles and scripts
*/
function apus_enqueue_header() {
// Header CSS
wp_enqueue_style(
'apus-header',
get_template_directory_uri() . '/assets/css/header.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 footer styles
*/
function apus_enqueue_footer_styles() {
wp_enqueue_style(
'apus-footer',
get_template_directory_uri() . '/assets/css/footer.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_footer_styles', 12);
/**
* Enqueue accessibility styles
*/
function apus_enqueue_accessibility() {
wp_enqueue_style(
'apus-accessibility',
get_template_directory_uri() . '/assets/css/accessibility.css',
array('apus-theme-style'),
'1.0.0',
'all'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_accessibility', 15);
/**
* Enqueue AdSense loader script
*/
function apus_enqueue_adsense_loader() {
// Only run on frontend
if (is_admin()) {
return;
}
// Check if AdSense delay is enabled
$delay_enabled = get_theme_mod('apus_adsense_delay_enabled', true);
if (!$delay_enabled) {
return;
}
// Enqueue AdSense loader script
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 Table of Contents styles and scripts
*/
function apus_enqueue_toc_assets() {
// Only enqueue on single posts
if (!is_single()) {
return;
}
// TOC CSS
wp_enqueue_style(
'apus-toc-style',
get_template_directory_uri() . '/assets/css/toc.css',
array('apus-bootstrap'),
APUS_VERSION,
'all'
);
// TOC JS
wp_enqueue_script(
'apus-toc-script',
get_template_directory_uri() . '/assets/js/toc.js',
array(),
APUS_VERSION,
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_toc_assets', 10);
/**
* Enqueue theme core styles
*/
function apus_enqueue_theme_styles() {
// Theme Core Styles
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/animations.css',
array('apus-theme'),
'1.0.0',
'all'
);
// Theme Responsive Styles
wp_enqueue_style(
'apus-responsive',
get_template_directory_uri() . '/assets/css/responsive.css',
array('apus-theme'),
'1.0.0',
'all'
);
// Theme Utilities
wp_enqueue_style(
'apus-utilities',
get_template_directory_uri() . '/assets/css/utilities.css',
array('apus-theme'),
'1.0.0',
'all'
);
// Print Styles
wp_enqueue_style(
'apus-print',
get_template_directory_uri() . '/assets/css/print.css',
array(),
'1.0.0',
'print'
);
}
add_action('wp_enqueue_scripts', 'apus_enqueue_theme_styles', 13);

View File

@@ -0,0 +1,150 @@
<?php
/**
* Featured Image Functions
*
* Configurable featured image display with lazy loading support.
*
* @package Apus_Theme
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Get Featured Image HTML
*
* Returns the HTML for the featured image based on theme options.
* Supports lazy loading and configurable display per post type.
*
* @param int $post_id Optional. Post ID. Defaults to current post.
* @param string $size Optional. Image size. Default 'apus-featured-large'.
* @param array $attr Optional. Additional attributes for the image.
* @param bool $force_show Optional. Force display regardless of settings. Default false.
* @return string HTML markup for featured image or empty string.
*/
function apus_get_featured_image($post_id = null, $size = 'apus-featured-large', $attr = array(), $force_show = false) {
// Get post ID if not provided
if (!$post_id) {
$post_id = get_the_ID();
}
// Check if post has thumbnail
if (!has_post_thumbnail($post_id)) {
return '';
}
// Get post type
$post_type = get_post_type($post_id);
// Check if featured images are enabled for this post type
if (!$force_show) {
$option_key = 'apus_featured_image_' . $post_type;
$enabled = get_theme_mod($option_key, true); // Default enabled
if (!$enabled) {
return '';
}
}
// Default attributes
$default_attr = array(
'alt' => get_the_title($post_id),
'loading' => 'lazy',
'class' => 'featured-image',
);
// Merge with custom attributes
$attributes = wp_parse_args($attr, $default_attr);
// Get the thumbnail HTML
$thumbnail = get_the_post_thumbnail($post_id, $size, $attributes);
if (empty($thumbnail)) {
return '';
}
// Wrap in container div
$output = '<div class="post-thumbnail">';
$output .= $thumbnail;
$output .= '</div>';
return $output;
}
/**
* Display Featured Image
*
* Echoes the featured image HTML.
*
* @param int $post_id Optional. Post ID. Defaults to current post.
* @param string $size Optional. Image size. Default 'apus-featured-large'.
* @param array $attr Optional. Additional attributes for the image.
* @param bool $force_show Optional. Force display regardless of settings. Default false.
*/
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);
}
/**
* Check if Featured Images are Enabled for Post Type
*
* @param string $post_type Optional. Post type. Defaults to current post type.
* @return bool True if enabled, false otherwise.
*/
function apus_is_featured_image_enabled($post_type = '') {
if (empty($post_type)) {
$post_type = get_post_type();
}
$option_key = 'apus_featured_image_' . $post_type;
return (bool) get_theme_mod($option_key, true);
}
/**
* Register Featured Image Settings in Customizer
*
* Adds controls to enable/disable featured images per post type.
*
* @param WP_Customize_Manager $wp_customize Theme Customizer object.
*/
function apus_featured_image_customizer($wp_customize) {
// Add section
$wp_customize->add_section('apus_featured_images', array(
'title' => __('Featured Images', 'apus-theme'),
'priority' => 30,
));
// Get public post types
$post_types = get_post_types(array('public' => true), 'objects');
foreach ($post_types as $post_type) {
// Skip attachments
if ($post_type->name === 'attachment') {
continue;
}
$setting_id = 'apus_featured_image_' . $post_type->name;
// Add setting
$wp_customize->add_setting($setting_id, array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
'transport' => 'refresh',
));
// Add control
$wp_customize->add_control($setting_id, array(
'label' => sprintf(
/* translators: %s: post type label */
__('Enable for %s', 'apus-theme'),
$post_type->labels->name
),
'section' => 'apus_featured_images',
'type' => 'checkbox',
));
}
}
add_action('customize_register', 'apus_featured_image_customizer');

View File

@@ -0,0 +1,352 @@
<?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;
}
/**
* Disable big image threshold for high-quality images
* WordPress 5.3+ scales down images larger than 2560px by default
* Uncomment to disable this behavior if you need full-size images
*/
// add_filter('big_image_size_threshold', '__return_false');
/**
* 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);

View File

@@ -0,0 +1,468 @@
<?php
/**
* Performance Optimization Functions
*
* Functions to remove WordPress bloat and improve performance.
*
* @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_filter( 'wp_resource_hints', 'apus_disable_emojis_remove_dns_prefetch', 10, 2 );
}
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();
}
/**
* Remove emoji CDN hostname from DNS prefetching hints
*
* @since 1.0.0
* @param array $urls URLs to print for resource hints.
* @param string $relation_type The relation type the URLs are printed for.
* @return array Modified array of URLs.
*/
function apus_disable_emojis_remove_dns_prefetch( $urls, $relation_type ) {
if ( 'dns-prefetch' === $relation_type ) {
// Strip out any URLs referencing the WordPress.org emoji location.
$emoji_svg_url_bit = 'https://s.w.org/images/core/emoji/';
foreach ( $urls as $key => $url ) {
if ( strpos( $url, $emoji_svg_url_bit ) !== false ) {
unset( $urls[ $key ] );
}
}
}
return $urls;
}
/**
* 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' );
/**
* Disable REST API for non-logged users
*
* Restricts REST API access to authenticated users only.
* Comment out if you need public REST API access.
*
* @since 1.0.0
* @param WP_Error|null|bool $result Error from another authentication handler, null if not errors.
* @return WP_Error|null|bool
*/
function apus_disable_rest_api( $result ) {
if ( ! empty( $result ) ) {
return $result;
}
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_not_logged_in',
esc_html__( 'You are not currently logged in.', 'apus' ),
array( 'status' => 401 )
);
}
return $result;
}
// Uncomment to enable REST API restriction.
// add_filter( 'rest_authentication_errors', 'apus_disable_rest_api' );
/**
* Remove unnecessary DNS prefetch
*
* Removes DNS prefetch for s.w.org (WordPress.org stats).
*
* @since 1.0.0
* @param array $hints DNS prefetch hints.
* @param string $relation_type The relation type.
* @return array Modified hints.
*/
function apus_remove_dns_prefetch( $hints, $relation_type ) {
if ( 'dns-prefetch' === $relation_type ) {
return array_diff( wp_dependencies_unique_hosts(), $hints );
}
return $hints;
}
add_filter( 'wp_resource_hints', 'apus_remove_dns_prefetch', 10, 2 );
/**
* Disable heartbeat API
*
* The heartbeat API can cause high CPU usage.
* This limits it to the post editor only.
*
* @since 1.0.0
* @param array $settings Heartbeat settings.
* @return array Modified settings.
*/
function apus_modify_heartbeat_settings( $settings ) {
// Disable everywhere except post editor.
global $pagenow;
if ( 'post.php' !== $pagenow && 'post-new.php' !== $pagenow ) {
wp_deregister_script( 'heartbeat' );
}
return $settings;
}
add_filter( 'heartbeat_settings', 'apus_modify_heartbeat_settings' );
/**
* Disable WordPress Core Lazy Loading
*
* WordPress 5.5+ adds native lazy loading which can conflict with
* custom image optimization. Only disable if using custom lazy loading.
*
* @since 1.0.0
* @param string $value Loading attribute value.
* @return string Modified loading attribute.
*/
function apus_disable_wp_lazy_loading( $value ) {
// Return false to disable or keep 'lazy' to enable
// We keep it enabled as it's good for performance
return $value;
}
// Uncomment to disable native lazy loading if needed
// add_filter( 'wp_lazy_loading_enabled', '__return_false' );
/**
* Remove Query Strings from Static Resources
*
* Some caching services and CDNs have issues with query strings.
* This removes version query strings from CSS and JS files.
*
* @since 1.0.0
* @param string $src Source URL.
* @return string Modified source URL.
*/
function apus_remove_query_strings( $src ) {
if ( strpos( $src, '?ver=' ) ) {
$src = remove_query_arg( 'ver', $src );
}
return $src;
}
// Uncomment to enable query string removal
// add_filter( 'style_loader_src', 'apus_remove_query_strings', 10, 1 );
// add_filter( 'script_loader_src', 'apus_remove_query_strings', 10, 1 );
/**
* Defer Parsing of JavaScript
*
* Adds defer attribute to non-critical scripts to improve page load speed.
*
* @since 1.0.0
* @param string $tag The script tag.
* @param string $handle The script handle.
* @param string $src The script source URL.
* @return string Modified script tag.
*/
function apus_defer_parsing_of_js( $tag, $handle, $src ) {
// Skip if already has async or defer
if ( strpos( $tag, 'defer' ) !== false || strpos( $tag, 'async' ) !== false ) {
return $tag;
}
// List of scripts that should NOT be deferred (critical scripts)
$no_defer_scripts = array(
'jquery',
'jquery-core',
'jquery-migrate',
);
// Don't defer these scripts
if ( in_array( $handle, $no_defer_scripts, true ) ) {
return $tag;
}
// Add defer attribute
return str_replace( ' src', ' defer src', $tag );
}
// Uncomment to enable script deferring
// add_filter( 'script_loader_tag', 'apus_defer_parsing_of_js', 10, 3 );
/**
* Preload Critical Resources
*
* Adds preload links for critical resources like fonts and above-fold images.
*
* @since 1.0.0
*/
function apus_preload_critical_resources() {
// Preload critical fonts (update paths as needed)
// Example:
// echo '<link rel="preload" href="' . get_template_directory_uri() . '/assets/fonts/font.woff2" as="font" type="font/woff2" crossorigin>';
// You can add more preload directives here based on your theme's needs
}
add_action( 'wp_head', 'apus_preload_critical_resources', 1 );
/**
* Add DNS Prefetch for External Domains
*
* Adds DNS prefetch hints for external resources to speed up loading.
*
* @since 1.0.0
*/
function apus_add_dns_prefetch() {
// Add DNS prefetch for common external resources
echo '<link rel="dns-prefetch" href="//fonts.googleapis.com">' . "\n";
echo '<link rel="dns-prefetch" href="//fonts.gstatic.com">' . "\n";
// Add more as needed for your external resources
}
add_action( 'wp_head', 'apus_add_dns_prefetch', 0 );
/**
* 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' );

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');

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';
}

View File

@@ -0,0 +1,525 @@
<?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
}

View File

@@ -0,0 +1,318 @@
<?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');
}

View File

@@ -0,0 +1,225 @@
<?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) {
if (empty($headings) || count($headings) < 2) {
return ''; // Don't show TOC if there are fewer than 2 headings
}
$toc_html = '<nav class="apus-toc" aria-label="' . esc_attr__('Table of Contents', 'apus-theme') . '">';
$toc_html .= '<div class="apus-toc-header">';
$toc_html .= '<h2 class="apus-toc-title">' . esc_html__('Table of Contents', 'apus-theme') . '</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;
}
/**
* Display Table of Contents before post content
*
* Hooks into apus_before_post_content to display TOC on single posts.
*/
function apus_display_toc() {
// Only show on single posts
if (!is_single()) {
return;
}
global $post;
if (empty($post->post_content)) {
return;
}
// Extract headings from content
$headings = apus_extract_headings($post->post_content);
// Generate and display TOC
$toc = apus_generate_toc($headings);
if (!empty($toc)) {
echo $toc; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in apus_generate_toc()
}
}
add_action('apus_before_post_content', 'apus_display_toc');
/**
* 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);