El campo Repeater (Campo Repetidor) en Advanced Custom Fields (ACF Pro) es una de las herramientas más potentes y versátiles para desarrolladores WordPress. Permite a los editores gestionar listas de elementos estructurados, como perfiles de equipo, tablas de precios, preguntas frecuentes, cronogramas de proyectos o catálogos de características, sin necesidad de crear Custom Post Types independientes para cada fragmento de contenido.
Aprende más sobre nuestros servicios de desarrollo WordPress profesional en WPPoland.
Sin embargo, a la hora de maquetar estos bloques repetitivos surge un reto técnico habitual: ¿cómo aplicar estilos visuales diferenciados a filas concretas? Los diseñadores suelen exigir patrones alternados (diseño en zigzag con imagen a la izquierda y texto a la derecha en filas impares, e invertido en filas pares), colores de fondo con efecto cebra o resaltados destacados para cada tercer elemento.
En esta guía técnica de ingeniería examinaremos cómo iterar eficientemente con el ciclo have_rows(), cómo emplear get_row_index() y el operador módulo en PHP, cuándo delegar la presentación en selectores modernos CSS :nth-child() y cómo optimizar la base de datos wp_postmeta para evitar cuellos de botella en sitios con alto tráfico.
Arquitectura de datos y almacenamiento de ACF Repeater en wp_postmeta
Antes de escribir plantillas en PHP, es imprescindible comprender cómo almacena WordPress los datos de ACF. En lugar de una tabla relacional estructurada específicamente para repetidores, WordPress almacena metadatos en formato clave-valor dentro de la tabla wp_postmeta.
Cuando se define un campo Repeater denominado hitos_proyecto con dos subcampos (titulo_hito y resumen_hito) y el editor introduce 4 filas, ACF genera los siguientes registros en la base de datos:
- Un registro contador principal:
meta_key = 'hitos_proyecto'conmeta_value = '4'. - Registros de metadatos para la iteración 0:
hitos_proyecto_0_titulo_hitoyhitos_proyecto_0_resumen_hito. - Registros de metadatos para la iteración 1:
hitos_proyecto_1_titulo_hitoyhitos_proyecto_1_resumen_hito. - …junto con registros de referencia del sistema (
_hitos_proyecto_0_titulo_hito=field_65a12b8).
Para una lista de 10 filas con 4 subcampos, ACF genera más de 80 registros individuales en wp_postmeta para una única entrada. Comprender este volumen de almacenamiento explica por qué una iteración ineficiente o no optimizada puede sobrecargar MySQL y degradar el tiempo de respuesta del servidor (TTFB).
Funciones esenciales del ciclo: have_rows, the_row y get_sub_field
La estructura estándar y recomendada para renderizar un campo repetidor se basa en have_rows() y the_row():
<?php if ( have_rows( 'hitos_proyecto' ) ) : ?>
<section class="hitos-cronologia" aria-label="<?php esc_attr_e( 'Hitos del proyecto', 'wppoland' ); ?>">
<div class="hitos-contenedor">
<?php
while ( have_rows( 'hitos_proyecto' ) ) :
the_row();
$titulo = get_sub_field( 'titulo_hito' );
$resumen = get_sub_field( 'resumen_hito' );
$imagen = get_sub_field( 'imagen_hito' );
?>
<article class="hito-tarjeta">
<?php if ( ! empty( $imagen ) && is_array( $imagen ) ) : ?>
<div class="hito-media">
<?php echo wp_get_attachment_image( (int) $imagen['id'], 'medium_large' ); ?>
</div>
<?php endif; ?>
<div class="hito-contenido">
<?php if ( ! empty( $titulo ) ) : ?>
<h3 class="hito-titulo"><?php echo esc_html( $titulo ); ?></h3>
<?php endif; ?>
<?php if ( ! empty( $resumen ) ) : ?>
<div class="hito-descripcion">
<?php echo wp_kses_post( $resumen ); ?>
</div>
<?php endif; ?>
</div>
</article>
<?php endwhile; ?>
</div>
</section>
<?php endif; ?>La función the_row() hace avanzar el cursor interno y configura el ámbito de datos de la fila actual. Dentro de este bucle, siempre debe utilizarse get_sub_field() en lugar de get_field(), ya que esta última intentará obtener valores de la entrada global en lugar de los subcampos locales.
Lógica condicional en PHP con get_row_index y operador modulo
Cuando las filas repetidas requieren clases dinámicas, atributos de datos específicos o variación de estructuras HTML, la lógica en PHP proporciona control absoluto.
Como funciona get_row_index en ACF
En lugar de inicializar contadores manuales ($i = 0), ACF incluye la función nativa get_row_index(). Esta función devuelve la posición de la fila activa como un índice basado en 1 (la primera fila devuelve 1, la segunda 2):
<?php if ( have_rows( 'pasos_proceso' ) ) : ?>
<ol class="proceso-lista">
<?php
while ( have_rows( 'pasos_proceso' ) ) :
the_row();
$indice = get_row_index();
$es_par = ( $indice % 2 === 0 );
$clase_fila = $es_par ? 'fila-invertida fila-par' : 'fila-estandar fila-impar';
?>
<li class="paso-item <?php echo esc_attr( $clase_fila ); ?>" data-paso="<?php echo (int) $indice; ?>">
<span class="paso-numero"><?php echo esc_html( sprintf( '%02d', $indice ) ); ?></span>
<div class="paso-cuerpo">
<h4><?php echo esc_html( get_sub_field( 'encabezado_paso' ) ); ?></h4>
<p><?php echo esc_html( get_sub_field( 'detalle_paso' ) ); ?></p>
</div>
</li>
<?php endwhile; ?>
</ol>
<?php endif; ?>Destacar cada tercera fila con el operador modulo
El operador módulo (%) obtiene el resto de la división entera, permitiendo crear cadencias visuales complejas:
- Filas alternas pares e impares:
$indice % 2 === 0 - Destacar cada tercer elemento en rejillas de tres columnas:
$indice % 3 === 0 - Estilos específicos para el primer y último registro: comprobación de
$indice === 1y comparación con el recuento total.
Ejemplo para asignar estilos promocionales cada tres tarjetas:
$modulo_tres = $indice % 3;
if ( $modulo_tres === 0 ) {
$clase_tarjeta = 'tarjeta-destacada tarjeta-terciaria';
} elseif ( $modulo_tres === 1 ) {
$clase_tarjeta = 'tarjeta-estandar tarjeta-primaria';
} else {
$clase_tarjeta = 'tarjeta-estandar tarjeta-secundaria';
}Estilizar con precision el primer y ultimo elemento
Para aplicar un remate visual inicial o suprimir separadores inferiores en la última tarjeta de la lista:
<?php
$datos_crudos = get_field( 'miembros_equipo' );
$total_filas = is_array( $datos_crudos ) ? count( $datos_crudos ) : 0;
if ( have_rows( 'miembros_equipo' ) ) :
?>
<div class="equipo-rejilla">
<?php
while ( have_rows( 'miembros_equipo' ) ) :
the_row();
$posicion = get_row_index();
$clases = [ 'miembro-tarjeta' ];
if ( $posicion === 1 ) {
$clases[] = 'tarjeta-primera';
}
if ( $posicion === $total_filas ) {
$clases[] = 'tarjeta-ultima';
}
if ( $posicion % 2 === 0 ) {
$clases[] = 'fila-par';
} else {
$clases[] = 'fila-impar';
}
?>
<div class="<?php echo esc_attr( implode( ' ', $clases ) ); ?>">
<p class="nombre"><?php echo esc_html( get_sub_field( 'nombre_completo' ) ); ?></p>
<p class="puesto"><?php echo esc_html( get_sub_field( 'puesto_laboral' ) ); ?></p>
</div>
<?php endwhile; ?>
</div>
<?php endif; ?>Lógica basada en subcampos y variaciones de diseño
La personalización no tiene por qué limitarse al orden numérico. Combinar el índice de la fila con los valores de subcampos configurados por el editor permite alternar diseños dinámicamente:
<?php if ( have_rows( 'secciones_caso' ) ) : ?>
<div class="caso-flujo-contenedor">
<?php
while ( have_rows( 'secciones_caso' ) ) :
the_row();
$fila_idx = get_row_index();
$formato = get_sub_field( 'estilo_diseno' ); // 'ancho-completo', 'dividido', 'compacto'
$color_acento = get_sub_field( 'tono_acento' );
$clases_contenedor = [
'seccion-caso',
'fila-' . $fila_idx,
'formato-' . sanitize_html_class( $formato ?: 'estandar' )
];
if ( $fila_idx % 2 === 0 ) {
$clases_contenedor[] = 'columnas-invertidas';
}
?>
<section
class="<?php echo esc_attr( implode( ' ', $clases_contenedor ) ); ?>"
style="<?php echo $color_acento ? esc_attr( '--acento-seccion: ' . sanitize_hex_color( $color_acento ) . ';' ) : ''; ?>"
>
<?php
if ( $formato === 'ancho-completo' && $fila_idx === 1 ) {
get_template_part( 'template-parts/casos/hero' );
} else {
get_template_part( 'template-parts/casos/tarjeta' );
}
?>
</section>
<?php endwhile; ?>
</div>
<?php endif; ?>El uso de variables CSS (--acento-seccion) garantiza una separación limpia entre la lógica del servidor y las reglas de diseño.
Cuando usar selectores CSS nth-child en lugar de PHP
Siguiendo el principio de separación de responsabilidades, cuando los cambios son puramente estéticos (colores de fondo, bordes, sombras o inversión de flexbox), el uso de pseudoclases CSS :nth-child() resulta más eficiente y mantiene limpias las plantillas PHP:
/* Efecto cebra para filas pares */
.proceso-lista .paso-item:nth-child(even) {
background-color: #f8fafc;
}
/* Inversion de orden de columnas en Flexbox */
.proceso-lista .paso-item {
display: flex;
flex-direction: row;
gap: 2rem;
align-items: center;
}
.proceso-lista .paso-item:nth-child(even) {
flex-direction: row-reverse;
}
/* Destacar cada tercera tarjeta en rejilla CSS Grid */
.hitos-contenedor .hito-tarjeta:nth-child(3n) {
border: 2px solid #2563eb;
transform: scale(1.02);
}
/* Reglas para primera y ultima tarjeta */
.proceso-lista .paso-item:first-child {
border-top: 3px solid #0f172a;
}
.proceso-lista .paso-item:last-child {
border-bottom: 3px solid #0f172a;
}Criterios de decision entre CSS y PHP
- CSS
:nth-child(): Ideal para alternar fondos, sombras, bordes decorativos e inversión de flexbox con media queries adaptadas a dispositivos móviles. - PHP módulo / índices: Imprescindible cuando cambia el marcado HTML semántico, cuando se cargan diferentes archivos parciales con
get_template_part()o cuando se calculan datos estructurados Schema.org.
Integración con ACF Flexible Content para estructuras modulares
Cuando las secciones requieren campos completamente dispares, forzar todas las opciones dentro de un único repetidor genera confusión en el panel de administración. El campo Flexible Content proporciona la estructura adecuada:
<?php if ( have_rows( 'modulos_pagina' ) ) : ?>
<main class="modulos-flujo">
<?php
while ( have_rows( 'modulos_pagina' ) ) :
the_row();
$tipo_layout = get_row_layout();
$posicion = get_row_index();
?>
<section class="modulo-item modulo-<?php echo esc_attr( $tipo_layout ); ?>" data-pos="<?php echo (int) $posicion; ?>">
<?php
if ( $tipo_layout === 'bloque_cabecera' ) :
get_template_part( 'template-parts/bloques/hero', null, [ 'indice' => $posicion ] );
elseif ( $tipo_layout === 'rejilla_servicios' ) :
get_template_part( 'template-parts/bloques/servicios', null, [ 'indice' => $posicion ] );
elseif ( $tipo_layout === 'carrusel_testimonios' ) :
get_template_part( 'template-parts/bloques/testimonios', null, [ 'indice' => $posicion ] );
endif;
?>
</section>
<?php endwhile; ?>
</main>
<?php endif; ?>La función get_row_layout() mantiene el código modular y previene registros vacíos innecesarios en MySQL.
Bloques Gutenberg nativos con ACF Blocks, InnerBlocks y Block Context
En los temas modernos basados en bloques, los repetidores clásicos se reemplazan con bloques Gutenberg personalizados registrados mediante block.json. La integración de InnerBlocks y Block Context permite crear rejillas modulares con edición visual directa.
Configuración en block.json
El archivo block.json del bloque hijo declara la relación con el bloque contenedor:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "wppoland/tarjeta-etapa",
"title": "Tarjeta de Etapa",
"category": "design",
"parent": [ "wppoland/contenedor-etapas" ],
"usesContext": [ "wppoland/formatoRejilla" ],
"acf": {
"mode": "preview",
"renderTemplate": "render.php"
}
}Renderizado de contenedor e items
En la plantilla del bloque padre (contenedor-etapas/render.php), se utiliza <InnerBlocks />:
<div class="contenedor-etapas-bloque">
<InnerBlocks
allowedBlocks="<?php echo esc_attr( wp_json_encode( ['wppoland/tarjeta-etapa'] ) ); ?>"
template="<?php echo esc_attr( wp_json_encode( [ ['wppoland/tarjeta-etapa'], ['wppoland/tarjeta-etapa'] ] ) ); ?>"
/>
</div>En la plantilla del bloque hijo (tarjeta-etapa/render.php), se lee el contexto recibido:
<?php
$formato = $context['wppoland/formatoRejilla'] ?? 'rejilla-3';
$titulo = get_field( 'titulo_tarjeta' );
$texto = get_field( 'texto_tarjeta' );
?>
<div class="tarjeta-etapa-item formato-<?php echo esc_attr( $formato ); ?>">
<h4><?php echo esc_html( $titulo ); ?></h4>
<div><?php echo wp_kses_post( $texto ); ?></div>
</div>Como optimizar el rendimiento y evitar sobrecarga en wp_postmeta
Los repetidores con decenas de filas o estructuras anidadas multiplican los registros en la tabla wp_postmeta. Un repetidor de 15 filas con un repetidor interno de 10 filas genera más de 500 registros para una sola página.
1. Lectura en bloque mediante get_field
Para conjuntos voluminosos de datos, obtener el array completo mediante una única llamada a get_field() resulta mucho más rápido que ejecutar cientos de llamadas a the_row() y get_sub_field():
$todos_pasos = get_field( 'pasos_proceso' );
if ( ! empty( $todos_pasos ) && is_array( $todos_pasos ) ) {
echo '<div class="lista-array-directo">';
foreach ( $todos_pasos as $cero_idx => $fila ) {
$numero = $cero_idx + 1;
$clase = ( $numero % 2 === 0 ) ? 'fila-par' : 'fila-impar';
$titulo = $fila['encabezado_paso'] ?? '';
$texto = $fila['detalle_paso'] ?? '';
echo '<div class="' . esc_attr( $clase ) . '">';
echo '<h4>' . esc_html( $titulo ) . '</h4>';
echo '<p>' . esc_html( $texto ) . '</p>';
echo '</div>';
}
echo '</div>';
}2. Cache del HTML renderizado con la API Transients
Para módulos que cambian con poca frecuencia, almacenar el HTML generado en cache con Transients reduce el tiempo de respuesta a milisegundos:
function wppoland_obtener_hitos_en_cache( int $post_id ): string {
$clave_cache = 'wppoland_hitos_html_' . $post_id;
$html_salida = get_transient( $clave_cache );
if ( false !== $html_salida ) {
return $html_salida;
}
if ( ! have_rows( 'hitos_proyecto', $post_id ) ) {
return '';
}
ob_start();
echo '<div class="hitos-rejilla-cache">';
while ( have_rows( 'hitos_proyecto', $post_id ) ) {
the_row();
$idx = get_row_index();
$clase = ( $idx % 2 === 0 ) ? 'tarjeta-par' : 'tarjeta-impar';
echo '<div class="' . esc_attr( $clase ) . '">';
echo '<h3>' . esc_html( get_sub_field( 'titulo_hito' ) ) . '</h3>';
echo '<div>' . wp_kses_post( get_sub_field( 'resumen_hito' ) ) . '</div>';
echo '</div>';
}
echo '</div>';
$html_salida = ob_get_clean();
// Guardar durante 24 horas (86400 segundos)
set_transient( $clave_cache, $html_salida, 86400 );
return $html_salida;
}
// Limpiar cache al actualizar la entrada
add_action( 'save_post', function( int $post_id ): void {
if ( ! wp_is_post_revision( $post_id ) && ! wp_is_post_autosave( $post_id ) ) {
delete_transient( 'wppoland_hitos_html_' . $post_id );
}
} );Optimización de datos en WP REST API y arquitecturas Headless
En arquitecturas desacopladas con frameworks como Astro, Next.js o Nuxt, registrar campos REST optimizados con register_rest_field aligera las respuestas JSON enviadas al frontend:
add_action( 'rest_api_init', function(): void {
register_rest_field( 'page', 'hitos_optimizados_api', [
'get_callback' => function( array $post_arr ): array {
$post_id = $post_arr['id'];
$filas = get_field( 'hitos_proyecto', $post_id );
if ( empty( $filas ) || ! is_array( $filas ) ) {
return [];
}
return array_map( function( array $fila, int $indice ): array {
return [
'posicion' => $indice + 1,
'esPar' => ( ( $indice + 1 ) % 2 === 0 ),
'titulo' => sanitize_text_field( $fila['titulo_hito'] ?? '' ),
'resumen' => wp_kses_post( $fila['resumen_hito'] ?? '' ),
];
}, $filas, array_keys( $filas ) );
},
'schema' => [
'description' => 'Listado normalizado de hitos para frontend headless.',
'type' => 'array',
],
] );
} );Ejemplo practico: tabla de precios con plan recomendado
A continuación, mostramos la implementación completa de una tabla de precios profesional:
<?php if ( have_rows( 'planes_precios' ) ) :
$todos_planes = get_field( 'planes_precios' );
$total_planes = is_array( $todos_planes ) ? count( $todos_planes ) : 0;
?>
<section class="tabla-precios-seccion" id="precios" aria-label="<?php esc_attr_e( 'Tabla de precios', 'wppoland' ); ?>">
<div class="precios-rejilla">
<?php
while ( have_rows( 'planes_precios' ) ) :
the_row();
$i = get_row_index();
$nombre_plan = get_sub_field( 'nombre_plan' );
$importe_plan = get_sub_field( 'importe_plan' );
$es_destacado = (bool) get_sub_field( 'marcar_como_recomendado' );
$caracteristicas = get_sub_field( 'lista_caracteristicas' );
$clases = [ 'precio-tarjeta', 'posicion-' . $i ];
if ( $es_destacado ) {
$clases[] = 'tarjeta-recomendada';
}
if ( $i === 1 ) {
$clases[] = 'tarjeta-inicial';
}
if ( $i === $total_planes ) {
$clases[] = 'tarjeta-enterprise';
}
$clases[] = ( $i % 2 === 0 ) ? 'sombra-par' : 'sombra-impar';
?>
<div class="<?php echo esc_attr( implode( ' ', $clases ) ); ?>">
<?php if ( $es_destacado ) : ?>
<span class="insignia-destacada"><?php esc_html_e( 'Más popular', 'wppoland' ); ?></span>
<?php endif; ?>
<h3 class="plan-titulo"><?php echo esc_html( $nombre_plan ); ?></h3>
<div class="plan-importe"><?php echo esc_html( $importe_plan ); ?></div>
<?php if ( ! empty( $caracteristicas ) && is_array( $caracteristicas ) ) : ?>
<ul class="plan-ventajas">
<?php foreach ( $caracteristicas as $item ) : ?>
<li><?php echo esc_html( $item['texto_ventaja'] ?? '' ); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<div class="plan-accion">
<a href="#contacto" class="boton-adquirir"><?php esc_html_e( 'Seleccionar plan', 'wppoland' ); ?></a>
</div>
</div>
<?php endwhile; ?>
</div>
</section>
<?php endif; ?>Estilos con CSS Grid:
.precios-rejilla {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
align-items: stretch;
}
.precio-tarjeta {
border-radius: 12px;
padding: 2rem;
background: #ffffff;
border: 1px solid #e2e8f0;
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.precio-tarjeta:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px -8px rgba(15, 23, 42, 0.12);
}
.precio-tarjeta.tarjeta-recomendada {
border: 2px solid #2563eb;
position: relative;
box-shadow: 0 20px 25px -5px rgba(37, 99, 235, 0.1);
}
.precio-tarjeta.sombra-par {
background-color: #f8fafc;
}Diagnostico de errores y resolucion de problemas frecuentes
- Bucle infinito: Si se omite la llamada a
the_row(), el puntero nunca avanza y la condición del buclewhilese ejecuta sin fin hasta agotar la memoria. - Subcampos vacíos por get_field: Usar
get_field()dentro de un repetidor intenta acceder al contexto global de la entrada, devolviendo nulo o datos no relacionados. - Errores fatales con count() en PHP 8: En PHP 8, pasar variables no inicializadas o booleanas a
count()provoca un TypeError crítico. Siempre debe verificarse previamente conis_array(). - Selectores nth-child afectados por librerias externas: Si una biblioteca de carrusel inyecta etiquetas envolventes adicionales, el selector directo de hijos puede fallar. En esos casos, aplicar clases mediante PHP asegura la precisión del estilo.
Preguntas frecuentes sobre el estilo de ACF Repeater
Cual es la diferencia entre get_row_index y un contador manual
get_row_index comienza en 0 o en 1
Cuando conviene estilizar con CSS en lugar de PHP
Como afectan los repetidores anidados al rendimiento de la base de datos
Modularización con get_template_part y paso de argumentos
En temas WordPress empresariales, mantener la lógica de las tarjetas separada de las plantillas principales evita el código espagueti. Desde WordPress 5.5, la función get_template_part() permite pasar un array asociativo como tercer parámetro $args:
<?php if ( have_rows( 'servicios_lista' ) ) : ?>
<div class="servicios-rejilla-modular">
<?php while ( have_rows( 'servicios_lista' ) ) : the_row();
$posicion_idx = get_row_index();
$datos_tarjeta = [
'indice' => $posicion_idx,
'es_par' => ( $posicion_idx % 2 === 0 ),
'titulo' => get_sub_field( 'servicio_titulo' ),
'resumen' => get_sub_field( 'servicio_resumen' ),
'icono' => get_sub_field( 'servicio_icono' ),
];
get_template_part( 'template-parts/componentes/tarjeta-servicio', null, $datos_tarjeta );
?>
<?php endwhile; ?>
</div>
<?php endif; ?>Dentro del archivo template-parts/componentes/tarjeta-servicio.php, los datos se leen directamente desde $args:
<?php
/**
* Componente modular de tarjeta de servicio.
*
* @var array $args Parámetros pasados desde el bucle repetidor.
*/
$indice = (int) ( $args['indice'] ?? 1 );
$es_par = (bool) ( $args['es_par'] ?? false );
$titulo = $args['titulo'] ?? '';
$resumen = $args['resumen'] ?? '';
$icono = $args['icono'] ?? null;
$clase_estilo = $es_par ? 'tarjeta-servicio estilo-par' : 'tarjeta-servicio estilo-impar';
?>
<article class="<?php echo esc_attr( $clase_estilo ); ?>" data-index="<?php echo esc_attr( $indice ); ?>">
<?php if ( ! empty( $icono['id'] ) ) : ?>
<div class="icono-contenedor">
<?php echo wp_get_attachment_image( (int) $icono['id'], 'thumbnail' ); ?>
</div>
<?php endif; ?>
<h3 class="servicio-nombre"><?php echo esc_html( $titulo ); ?></h3>
<div class="servicio-detalle"><?php echo wp_kses_post( $resumen ); ?></div>
</article>Esta arquitectura permite probar las tarjetas en entornos aislados y reutilizarlas en diferentes secciones de la plataforma sin duplicar marcado.
Repetidores anidados y gestión de múltiples niveles
Un caso habitual en plataformas corporativas es la necesidad de un repetidor dentro de otro repetidor, como una lista de oficinas donde cada oficina contiene un listado de departamentos y empleados:
<?php if ( have_rows( 'sedes_empresa' ) ) : ?>
<div class="sedes-listado">
<?php while ( have_rows( 'sedes_empresa' ) ) : the_row();
$sede_idx = get_row_index();
$ciudad_sede = get_sub_field( 'ciudad' );
?>
<section class="sede-bloque sede-<?php echo esc_attr( $sede_idx ); ?>">
<h2 class="sede-titulo"><?php echo esc_html( $ciudad_sede ); ?></h2>
<?php if ( have_rows( 'contactos_sede' ) ) : ?>
<ul class="contactos-lista">
<?php while ( have_rows( 'contactos_sede' ) ) : the_row();
$contacto_idx = get_row_index();
$nombre_c = get_sub_field( 'nombre' );
$cargo_c = get_sub_field( 'cargo' );
$clase_contacto = ( $contacto_idx % 2 === 0 ) ? 'fila-b' : 'fila-a';
?>
<li class="contacto-item <?php echo esc_attr( $clase_contacto ); ?>">
<strong><?php echo esc_html( $nombre_c ); ?></strong>
<span><?php echo esc_html( $cargo_c ); ?></span>
</li>
<?php endwhile; ?>
</ul>
<?php endif; ?>
</section>
<?php endwhile; ?>
</div>
<?php endif; ?>La función get_row_index() invocada dentro del bucle interno contactos_sede devuelve la posición relativa del contacto dentro de su sede, facilitando patrones de cebra locales sin alterar la indexación global.
Impacto en Redis y consumo de memoria del servidor
En infraestructuras optimizadas con caché persistente de objetos (como Redis o Memcached), el impacto de campos repetidores masivos requiere una supervisión rigurosa.
Razones de saturacion en servidores de alto tráfico
- Serialización pesada en Object Cache: La función
update_postmeta_cache()agrupa todos los metadatos del post en un único objeto serializado. Con repetidores voluminosos que contienen imágenes e información en texto enriquecido, el tamaño del payload puede superar los 500 KB por entrada. - Ciclos de CPU en el intérprete PHP: Cada registro procesado por ACF activa filtros internos y resoluciones de esquema de campo. En bucles de 100 subcampos, el tiempo de ejecución en PHP aumenta significativamente.
- Escritura intensiva en base de datos: Durante la edición en el panel de control, ACF borra y vuelve a insertar las filas correspondientes en
wp_postmeta, generando bloqueos temporales de fila y fragmentación en bases de datos con concurrencia elevada.
Al implementar almacenamiento en cache de fragmentos HTML con Transients o convertir las estructuras repetitivas en bloques de Gutenberg con InnerBlocks, se elimina este cuello de botella y se preserva la estabilidad de la plataforma.
Seguridad y directrices de escape de datos en la salida
En el desarrollo de temas orientados a producción, la seguridad en la salida de datos debe mantenerse como prioridad absoluta:
- Textos sin formato: Asegura siempre los campos simples mediante
esc_html()oesc_attr(). - Enlaces y URLs: Pasa los enlaces por
esc_url()y añaderel="noopener noreferrer"en los hipervínculos externos. - Imágenes de la biblioteca de medios: Configura el subcampo para devolver el ID de la imagen y renderiza la etiqueta con
wp_get_attachment_image(), beneficiándote del atributo nativoloading="lazy"y un etiquetado adaptativo impecable. - Contenido enriquecido: Para campos WYSIWYG utiliza siempre
wp_kses_post().
Resumen técnico y buenas practicas de ingeniería
La implementación profesional de repetidores en ACF equilibra la sencillez de edición con el rendimiento del servidor. Al combinar índices nativos de PHP con selectores modernos de CSS y estrategias de cache, se obtienen soluciones robustas y escalables.
Puntos clave para recordar:
- Comprueba siempre los datos con
have_rows()y no olvidesthe_row(). - Lee los subcampos exclusivamente con
get_sub_field(). - Escapa todas las salidas con
esc_html(),esc_attr()ywp_kses_post(). - Delega la presentación cosmética en CSS con
:nth-child(). - Implementa cache con Transients o migra a bloques Gutenberg en listas voluminosas.
Si buscas asesoramiento especializado para optimizar temas WordPress complejos o implementar bloques a medida con Gutenberg, consulta nuestros servicios de mantenimiento WordPress y desarrollo web en WPPoland.







