Segunda ola de implementaciones masivas con agentes paralelos para funcionalidades avanzadas de SEO, accesibilidad y contenido especializado. **Issue #17 - Imágenes responsive con srcset/WebP/AVIF:** - inc/image-optimization.php: 8 nuevas funciones para optimización - Soporte WebP/AVIF con detección de servidor - Srcset y sizes automáticos contextuales - Lazy loading inteligente (excluye LCP) - Threshold 2560px para big images - Picture element con fallbacks - Preload de featured images - Calidad JPEG optimizada (85%) - Dimensiones explícitas (previene CLS) - 14 filtros WordPress implementados - Beneficios: 30-50% reducción con WebP, 50-70% con AVIF - Core Web Vitals: Mejora LCP y CLS **Issue #18 - Accesibilidad WCAG 2.1 AA:** - assets/css/accessibility.css: +461 líneas - Focus styles visibles (3px outline) - Screen reader utilities - Touch targets ≥44px - High contrast mode support - Reduced motion support - Color contrast AA (4.5:1, 3:1) - assets/js/accessibility.js: 19KB nuevo - Skip links con smooth scroll - Navegación por teclado en dropdowns - Arrow keys en menús WordPress - Modal keyboard support - Focus management y trap - ARIA live regions - Announcements para screen readers - header.php: ARIA labels en navbar - Actualizaciones JS: Respeto prefers-reduced-motion en main.js, toc.js, header.js - Cumplimiento completo WCAG 2.1 Level AA **Issue #30 - Tablas APU (Análisis Precios Unitarios):** - assets/css/tables-apu.css: 560 líneas - Diseño sin bordes, moderno - Zebra striping (#f8f9fa/#ffffff) - Headers sticky con degradado azul - 4 tipos de filas: normal, section-header, subtotal, total - Fuente monospace para columnas monetarias - Responsive (scroll horizontal móvil) - Print styles con color-adjust: exact - inc/apu-tables.php: 330 líneas, 6 funciones - apus_process_apu_tables() - Procesamiento automático - Shortcodes: [apu_table], [apu_row type=""] - apus_generate_apu_table($data) - Generación programática - 4 métodos de uso: data-apu, shortcode, clase manual, PHP - docs/APU-TABLES-GUIDE.md: Guía completa de usuario - docs/APU-TABLE-EXAMPLE.html: Ejemplo funcional - 6 columnas: Clave, Descripción, Unidad, Cantidad, Costo, Importe - CRÍTICO: Contenido principal del sitio de construcción **Issue #31 - Botones de compartir en redes sociales:** - inc/social-share.php: 127 líneas - apus_get_social_share_buttons() - Genera HTML - apus_display_social_share() - Template tag - 5 redes: Facebook, X/Twitter, LinkedIn, WhatsApp, Email - URLs nativas sin JavaScript de terceros - Encoding seguro, ARIA labels - assets/css/social-share.css: 137 líneas - Animaciones hover (translateY, scale) - Colores específicos por red - Responsive (576px, 360px) - Focus styles accesibles - single.php: Integración después del contenido - Bootstrap Icons CDN (v1.11.3) - Panel de opciones con configuración **Issue #33 - Schema.org completo (5 tipos):** - inc/schema-org.php: 468 líneas, 7 funciones - Organization schema con logo y redes sociales - WebSite schema con SearchAction - Article schema (posts) con autor, imagen, categorías, wordCount - WebPage schema (páginas) con featured image - BreadcrumbList schema (8 contextos diferentes) - JSON-LD format en <head> - Referencias cruzadas con @id - Google Rich Results compliant - Deshabilita schemas Rank Math/Yoast (evita duplicación) - Locale: es-MX - Hook: wp_head (prioridad 5) **Archivos Modificados:** - functions.php: Includes de nuevos módulos (schema-org, apu-tables, social-share) - inc/enqueue-scripts.php: Enqueue de nuevos CSS/JS, Bootstrap Icons CDN - inc/image-optimization.php: 8 funciones nuevas WebP/AVIF - assets/css/accessibility.css: +461 líneas - assets/js/main.js, toc.js, header.js: Reduced motion support - single.php: Social share buttons - header.php: ARIA labels - inc/admin/options-api.php: Social share settings **Archivos Creados:** - 3 archivos PHP funcionales (apu-tables, social-share, schema-org) - 1 archivo JavaScript (accessibility.js - 19KB) - 3 archivos CSS (tables-apu, social-share) - 2 archivos docs/ (APU guide y example) - 5 reportes .md de documentación **Estadísticas:** - Total funciones nuevas: 30+ - Líneas de código nuevas: 2,500+ - Archivos nuevos: 13 - Archivos modificados: 10 - Mejoras de accesibilidad: WCAG 2.1 AA compliant - Mejoras SEO: 5 schemas JSON-LD - Mejoras performance: WebP/AVIF, lazy loading, srcset 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
469 lines
13 KiB
PHP
469 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* Schema.org JSON-LD Implementation
|
|
*
|
|
* Implementa 5 tipos de schemas para mejorar el SEO:
|
|
* - Article (posts individuales)
|
|
* - WebPage (páginas estáticas)
|
|
* - BreadcrumbList (navegación)
|
|
* - Organization (información de la organización)
|
|
* - WebSite (información del sitio con SearchAction)
|
|
*
|
|
* @package Apus_Theme
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
// Exit if accessed directly
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Añade todos los schemas JSON-LD al <head>
|
|
*/
|
|
function apus_output_schema_jsonld() {
|
|
$schemas = array();
|
|
|
|
// Siempre incluir Organization y WebSite
|
|
$schemas[] = apus_get_organization_schema();
|
|
$schemas[] = apus_get_website_schema();
|
|
|
|
// Schemas específicos según el contexto
|
|
if (is_singular('post')) {
|
|
$schemas[] = apus_get_article_schema();
|
|
$schemas[] = apus_get_breadcrumb_schema();
|
|
} elseif (is_page()) {
|
|
$schemas[] = apus_get_webpage_schema();
|
|
$schemas[] = apus_get_breadcrumb_schema();
|
|
} elseif (is_front_page()) {
|
|
// La página principal ya tiene WebSite schema
|
|
$schemas[] = apus_get_breadcrumb_schema();
|
|
} else {
|
|
// Para archives, categorías, etc.
|
|
$schemas[] = apus_get_breadcrumb_schema();
|
|
}
|
|
|
|
// Filtrar schemas vacíos
|
|
$schemas = array_filter($schemas);
|
|
|
|
// Crear graph con todos los schemas
|
|
if (!empty($schemas)) {
|
|
$graph = array(
|
|
'@context' => 'https://schema.org',
|
|
'@graph' => $schemas
|
|
);
|
|
|
|
echo '<script type="application/ld+json">';
|
|
echo wp_json_encode($graph, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
echo '</script>' . "\n";
|
|
}
|
|
}
|
|
add_action('wp_head', 'apus_output_schema_jsonld', 5);
|
|
|
|
/**
|
|
* Genera el schema Organization
|
|
* Información sobre la organización/empresa
|
|
*
|
|
* @return array Schema Organization
|
|
*/
|
|
function apus_get_organization_schema() {
|
|
$logo = get_theme_mod('custom_logo');
|
|
$logo_url = '';
|
|
|
|
if ($logo) {
|
|
$logo_data = wp_get_attachment_image_src($logo, 'full');
|
|
if ($logo_data) {
|
|
$logo_url = $logo_data[0];
|
|
}
|
|
}
|
|
|
|
// Si no hay logo personalizado, usar un fallback
|
|
if (empty($logo_url)) {
|
|
$logo_url = get_template_directory_uri() . '/assets/images/logo.png';
|
|
}
|
|
|
|
$schema = array(
|
|
'@type' => 'Organization',
|
|
'@id' => home_url('/#organization'),
|
|
'name' => get_bloginfo('name'),
|
|
'url' => home_url('/'),
|
|
'logo' => array(
|
|
'@type' => 'ImageObject',
|
|
'url' => $logo_url,
|
|
'@id' => home_url('/#logo')
|
|
),
|
|
'description' => get_bloginfo('description'),
|
|
'sameAs' => array()
|
|
);
|
|
|
|
// Añadir redes sociales si están configuradas
|
|
$social_profiles = array(
|
|
'facebook' => get_theme_mod('social_facebook', ''),
|
|
'twitter' => get_theme_mod('social_twitter', ''),
|
|
'linkedin' => get_theme_mod('social_linkedin', ''),
|
|
'instagram' => get_theme_mod('social_instagram', ''),
|
|
'youtube' => get_theme_mod('social_youtube', '')
|
|
);
|
|
|
|
foreach ($social_profiles as $profile_url) {
|
|
if (!empty($profile_url)) {
|
|
$schema['sameAs'][] = $profile_url;
|
|
}
|
|
}
|
|
|
|
// Eliminar array vacío si no hay redes sociales
|
|
if (empty($schema['sameAs'])) {
|
|
unset($schema['sameAs']);
|
|
}
|
|
|
|
return $schema;
|
|
}
|
|
|
|
/**
|
|
* Genera el schema WebSite con SearchAction
|
|
* Información del sitio web y funcionalidad de búsqueda
|
|
*
|
|
* @return array Schema WebSite
|
|
*/
|
|
function apus_get_website_schema() {
|
|
$schema = array(
|
|
'@type' => 'WebSite',
|
|
'@id' => home_url('/#website'),
|
|
'url' => home_url('/'),
|
|
'name' => get_bloginfo('name'),
|
|
'description' => get_bloginfo('description'),
|
|
'publisher' => array(
|
|
'@id' => home_url('/#organization')
|
|
),
|
|
'inLanguage' => 'es-MX'
|
|
);
|
|
|
|
// Añadir SearchAction solo si la búsqueda está habilitada
|
|
// (el tema desactiva la búsqueda por defecto en Issue #3)
|
|
if (!get_option('apus_disable_search', false)) {
|
|
$schema['potentialAction'] = array(
|
|
'@type' => 'SearchAction',
|
|
'target' => array(
|
|
'@type' => 'EntryPoint',
|
|
'urlTemplate' => home_url('/?s={search_term_string}')
|
|
),
|
|
'query-input' => 'required name=search_term_string'
|
|
);
|
|
}
|
|
|
|
return $schema;
|
|
}
|
|
|
|
/**
|
|
* Genera el schema Article para posts
|
|
* Información completa del artículo
|
|
*
|
|
* @return array|null Schema Article o null si no es un post
|
|
*/
|
|
function apus_get_article_schema() {
|
|
if (!is_singular('post')) {
|
|
return null;
|
|
}
|
|
|
|
global $post;
|
|
$post_id = get_the_ID();
|
|
|
|
// Imagen destacada
|
|
$image_url = '';
|
|
$image_width = 1200;
|
|
$image_height = 630;
|
|
|
|
if (has_post_thumbnail($post_id)) {
|
|
$image_data = wp_get_attachment_image_src(get_post_thumbnail_id($post_id), 'full');
|
|
if ($image_data) {
|
|
$image_url = $image_data[0];
|
|
$image_width = $image_data[1];
|
|
$image_height = $image_data[2];
|
|
}
|
|
}
|
|
|
|
// Autor
|
|
$author_id = $post->post_author;
|
|
$author_name = get_the_author_meta('display_name', $author_id);
|
|
$author_url = get_author_posts_url($author_id);
|
|
$author_description = get_the_author_meta('description', $author_id);
|
|
|
|
// Categorías y palabras clave
|
|
$categories = get_the_category($post_id);
|
|
$category_names = array();
|
|
if (!empty($categories)) {
|
|
foreach ($categories as $category) {
|
|
$category_names[] = $category->name;
|
|
}
|
|
}
|
|
|
|
// Extracto o descripción
|
|
$description = get_the_excerpt($post_id);
|
|
if (empty($description)) {
|
|
$description = wp_trim_words(strip_tags($post->post_content), 55, '...');
|
|
}
|
|
|
|
$schema = array(
|
|
'@type' => 'Article',
|
|
'@id' => get_permalink($post_id) . '#article',
|
|
'headline' => get_the_title($post_id),
|
|
'description' => $description,
|
|
'datePublished' => get_the_date('c', $post_id),
|
|
'dateModified' => get_the_modified_date('c', $post_id),
|
|
'author' => array(
|
|
'@type' => 'Person',
|
|
'@id' => $author_url . '#person',
|
|
'name' => $author_name,
|
|
'url' => $author_url
|
|
),
|
|
'publisher' => array(
|
|
'@id' => home_url('/#organization')
|
|
),
|
|
'mainEntityOfPage' => array(
|
|
'@type' => 'WebPage',
|
|
'@id' => get_permalink($post_id)
|
|
),
|
|
'inLanguage' => 'es-MX',
|
|
'isPartOf' => array(
|
|
'@id' => home_url('/#website')
|
|
)
|
|
);
|
|
|
|
// Añadir imagen si existe
|
|
if (!empty($image_url)) {
|
|
$schema['image'] = array(
|
|
'@type' => 'ImageObject',
|
|
'url' => $image_url,
|
|
'width' => $image_width,
|
|
'height' => $image_height
|
|
);
|
|
}
|
|
|
|
// Añadir categorías como articleSection
|
|
if (!empty($category_names)) {
|
|
$schema['articleSection'] = $category_names;
|
|
$schema['keywords'] = implode(', ', $category_names);
|
|
}
|
|
|
|
// Añadir descripción del autor si existe
|
|
if (!empty($author_description)) {
|
|
$schema['author']['description'] = $author_description;
|
|
}
|
|
|
|
// Número de palabras
|
|
$word_count = str_word_count(strip_tags($post->post_content));
|
|
if ($word_count > 0) {
|
|
$schema['wordCount'] = $word_count;
|
|
}
|
|
|
|
return $schema;
|
|
}
|
|
|
|
/**
|
|
* Genera el schema WebPage para páginas estáticas
|
|
* Información de página genérica
|
|
*
|
|
* @return array|null Schema WebPage o null si no es una página
|
|
*/
|
|
function apus_get_webpage_schema() {
|
|
if (!is_page()) {
|
|
return null;
|
|
}
|
|
|
|
global $post;
|
|
$page_id = get_the_ID();
|
|
|
|
// Imagen destacada
|
|
$image_url = '';
|
|
if (has_post_thumbnail($page_id)) {
|
|
$image_data = wp_get_attachment_image_src(get_post_thumbnail_id($page_id), 'full');
|
|
if ($image_data) {
|
|
$image_url = $image_data[0];
|
|
}
|
|
}
|
|
|
|
// Extracto o descripción
|
|
$description = get_the_excerpt($page_id);
|
|
if (empty($description)) {
|
|
$description = wp_trim_words(strip_tags($post->post_content), 55, '...');
|
|
}
|
|
|
|
$schema = array(
|
|
'@type' => 'WebPage',
|
|
'@id' => get_permalink($page_id) . '#webpage',
|
|
'url' => get_permalink($page_id),
|
|
'name' => get_the_title($page_id),
|
|
'description' => $description,
|
|
'datePublished' => get_the_date('c', $page_id),
|
|
'dateModified' => get_the_modified_date('c', $page_id),
|
|
'isPartOf' => array(
|
|
'@id' => home_url('/#website')
|
|
),
|
|
'inLanguage' => 'es-MX'
|
|
);
|
|
|
|
// Añadir imagen si existe
|
|
if (!empty($image_url)) {
|
|
$schema['primaryImageOfPage'] = array(
|
|
'@type' => 'ImageObject',
|
|
'url' => $image_url
|
|
);
|
|
}
|
|
|
|
// Breadcrumb reference
|
|
$schema['breadcrumb'] = array(
|
|
'@id' => get_permalink($page_id) . '#breadcrumb'
|
|
);
|
|
|
|
return $schema;
|
|
}
|
|
|
|
/**
|
|
* Genera el schema BreadcrumbList
|
|
* Navegación de migas de pan
|
|
*
|
|
* @return array Schema BreadcrumbList
|
|
*/
|
|
function apus_get_breadcrumb_schema() {
|
|
$items = array();
|
|
$position = 1;
|
|
|
|
// Inicio (Home)
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => 'Inicio',
|
|
'item' => home_url('/')
|
|
);
|
|
|
|
// Para posts
|
|
if (is_singular('post')) {
|
|
$post_id = get_the_ID();
|
|
$categories = get_the_category($post_id);
|
|
|
|
// Añadir la primera categoría si existe
|
|
if (!empty($categories)) {
|
|
$category = $categories[0];
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => $category->name,
|
|
'item' => get_category_link($category->term_id)
|
|
);
|
|
}
|
|
|
|
// Post actual (sin item ya que es la página actual)
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => get_the_title($post_id)
|
|
);
|
|
}
|
|
// Para páginas
|
|
elseif (is_page()) {
|
|
global $post;
|
|
$page_id = get_the_ID();
|
|
|
|
// Si tiene páginas padre
|
|
if ($post->post_parent) {
|
|
$ancestors = array_reverse(get_post_ancestors($page_id));
|
|
foreach ($ancestors as $ancestor_id) {
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => get_the_title($ancestor_id),
|
|
'item' => get_permalink($ancestor_id)
|
|
);
|
|
}
|
|
}
|
|
|
|
// Página actual
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => get_the_title($page_id)
|
|
);
|
|
}
|
|
// Para categorías
|
|
elseif (is_category()) {
|
|
$category = get_queried_object();
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => $category->name
|
|
);
|
|
}
|
|
// Para archivos de autor
|
|
elseif (is_author()) {
|
|
$author = get_queried_object();
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => $author->display_name
|
|
);
|
|
}
|
|
// Para archivos de fecha
|
|
elseif (is_date()) {
|
|
if (is_year()) {
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => get_the_date('Y')
|
|
);
|
|
} elseif (is_month()) {
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => get_the_date('F Y')
|
|
);
|
|
} elseif (is_day()) {
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => get_the_date('d F Y')
|
|
);
|
|
}
|
|
}
|
|
// Para búsquedas
|
|
elseif (is_search()) {
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => 'Resultados de búsqueda para: ' . get_search_query()
|
|
);
|
|
}
|
|
// Para 404
|
|
elseif (is_404()) {
|
|
$items[] = array(
|
|
'@type' => 'ListItem',
|
|
'position' => $position++,
|
|
'name' => 'Página no encontrada'
|
|
);
|
|
}
|
|
|
|
$schema = array(
|
|
'@type' => 'BreadcrumbList',
|
|
'@id' => get_permalink() . '#breadcrumb',
|
|
'itemListElement' => $items
|
|
);
|
|
|
|
return $schema;
|
|
}
|
|
|
|
/**
|
|
* Deshabilita los schemas de Rank Math si está activo
|
|
* Para evitar duplicación de schemas
|
|
*/
|
|
function apus_disable_rankmath_schema() {
|
|
// Deshabilitar schema de Rank Math si está activo
|
|
if (class_exists('RankMath')) {
|
|
add_filter('rank_math/json_ld', '__return_false');
|
|
}
|
|
|
|
// Deshabilitar schema de Yoast SEO si está activo
|
|
if (defined('WPSEO_VERSION')) {
|
|
add_filter('wpseo_json_ld_output', '__return_false');
|
|
}
|
|
}
|
|
add_action('wp_head', 'apus_disable_rankmath_schema', 1);
|