feat(adsense): reorganizar panel con UX mejorada y soporte 1-8 ads random

Panel AdSense reorganizado:
- Diagrama visual mostrando ubicaciones de anuncios (POST-TOP, IN-CONTENT, POST-BOTTOM, RAIL)
- Secciones colapsables por ubicación con badges de color
- Slots con descripciones claras indicando uso (Auto, In-Article, Display, etc.)

In-Content Ads mejorado:
- Soporte para 1-8 anuncios dentro del contenido
- Modo aleatorio (random) que varía posiciones en cada visita
- Configuración de mínimo/máximo de ads
- Párrafos mínimos entre anuncios configurable (2-6)
- Primer ad siempre en posición fija configurada

Archivos modificados:
- Schema v1.2.0 con 4 nuevos campos (random_mode, min_ads, max_ads, min_paragraphs_between)
- FormBuilder con diagrama visual y mejor organización
- ContentAdInjector con lógica de posicionamiento random
- Renderer con soporte para post-content-1 hasta post-content-8
- FieldMapper actualizado con nuevos campos

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-27 20:45:40 -06:00
parent 1a069a1336
commit 55f061df67
5 changed files with 544 additions and 188 deletions

View File

@@ -8,6 +8,11 @@ use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsensePlacementRenderer;
/**
* Inyecta anuncios dentro del contenido del post
* via filtro the_content
*
* Soporta:
* - Modo aleatorio (random) con posiciones variables
* - Configuracion de 1-8 ads maximo
* - Espacio minimo entre anuncios
*/
final class ContentAdInjector
{
@@ -31,40 +36,166 @@ final class ContentAdInjector
return $content;
}
// Obtener configuracion
$minAds = (int)($this->settings['behavior']['post_content_min_ads'] ?? 1);
$maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 3);
$afterParagraphs = (int)($this->settings['behavior']['post_content_after_paragraphs'] ?? 3);
$maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 2);
$minBetween = (int)($this->settings['behavior']['post_content_min_paragraphs_between'] ?? 4);
$randomMode = ($this->settings['behavior']['post_content_random_mode'] ?? true) === true;
// Validar min <= max
if ($minAds > $maxAds) {
$minAds = $maxAds;
}
// Dividir contenido en parrafos
$paragraphs = explode('</p>', $content);
$paragraphs = $this->splitIntoParagraphs($content);
$totalParagraphs = count($paragraphs);
// Necesitamos al menos afterParagraphs + 1 parrafos
if ($totalParagraphs < $afterParagraphs + 1) {
return $content;
}
$adsInserted = 0;
// Calcular posiciones de insercion
$adPositions = $this->calculateAdPositions(
$totalParagraphs,
$afterParagraphs,
$minBetween,
$minAds,
$maxAds,
$randomMode
);
if (empty($adPositions)) {
return $content;
}
// Reconstruir contenido con anuncios insertados
return $this->buildContentWithAds($paragraphs, $adPositions);
}
/**
* Divide el contenido en parrafos preservando el HTML
*/
private function splitIntoParagraphs(string $content): array
{
// Dividir por </p>, pero mantener el tag
$parts = preg_split('/(<\/p>)/i', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
$paragraphs = [];
$current = '';
foreach ($parts as $part) {
$current .= $part;
if (strtolower($part) === '</p>') {
$paragraphs[] = $current;
$current = '';
}
}
// Si hay contenido restante (sin cerrar), agregarlo
if (!empty(trim($current))) {
$paragraphs[] = $current;
}
return $paragraphs;
}
/**
* Calcula las posiciones donde insertar anuncios
*
* @return int[] Indices de parrafos despues de los cuales insertar ads
*/
private function calculateAdPositions(
int $totalParagraphs,
int $afterFirst,
int $minBetween,
int $minAds,
int $maxAds,
bool $randomMode
): array {
// Calcular posiciones disponibles respetando el espacio minimo
$availablePositions = [];
$lastPosition = $afterFirst; // Primera posicion fija
// La primera posicion siempre es despues del parrafo indicado
if ($afterFirst < $totalParagraphs) {
$availablePositions[] = $afterFirst;
}
// Calcular posiciones adicionales respetando minBetween
$nextPosition = $afterFirst + $minBetween;
while ($nextPosition < $totalParagraphs - 1) { // -1 para no insertar al final
$availablePositions[] = $nextPosition;
$nextPosition += $minBetween;
}
// Determinar cuantos ads insertar
$maxPossible = count($availablePositions);
if ($maxPossible === 0) {
return [];
}
// Limitar por maxAds y lo que el contenido permite
$actualMax = min($maxAds, $maxPossible);
$actualMin = min($minAds, $actualMax);
// Determinar cantidad final de ads
if ($randomMode) {
// En modo random, elegir cantidad aleatoria entre min y max
$numAds = rand($actualMin, $actualMax);
} else {
// En modo fijo, usar el maximo posible
$numAds = $actualMax;
}
if ($numAds === 0) {
return [];
}
// Seleccionar posiciones
if ($randomMode && $numAds < $maxPossible) {
// Modo random: elegir posiciones aleatorias
// Siempre incluir la primera posicion
$selectedPositions = [$availablePositions[0]];
if ($numAds > 1) {
// Elegir aleatoriamente del resto
$remainingPositions = array_slice($availablePositions, 1);
shuffle($remainingPositions);
$additionalPositions = array_slice($remainingPositions, 0, $numAds - 1);
$selectedPositions = array_merge($selectedPositions, $additionalPositions);
}
// Ordenar para insertar en orden correcto
sort($selectedPositions);
return $selectedPositions;
} else {
// Modo fijo o todas las posiciones necesarias
return array_slice($availablePositions, 0, $numAds);
}
}
/**
* Reconstruye el contenido insertando anuncios en las posiciones indicadas
*/
private function buildContentWithAds(array $paragraphs, array $adPositions): string
{
$newContent = '';
$adsInserted = 0;
foreach ($paragraphs as $index => $paragraph) {
$newContent .= $paragraph;
if ($index < $totalParagraphs - 1) {
$newContent .= '</p>';
}
// Verificar si debemos insertar un ad despues de este parrafo
// El indice es 0-based, las posiciones son 1-based (parrafo #3 = index 2)
$paragraphNumber = $index + 1;
// Primer anuncio despues del parrafo indicado
if ($paragraphNumber === $afterParagraphs && $adsInserted < $maxAds) {
$newContent .= $this->renderer->renderSlot($this->settings, 'post-content-' . ($adsInserted + 1));
$adsInserted++;
}
// Segundo anuncio a mitad del contenido restante
$midPoint = $afterParagraphs + (int)(($totalParagraphs - $afterParagraphs) / 2);
if ($paragraphNumber === $midPoint && $adsInserted < $maxAds && $maxAds > 1) {
$newContent .= $this->renderer->renderSlot($this->settings, 'post-content-' . ($adsInserted + 1));
if (in_array($paragraphNumber, $adPositions, true)) {
$adsInserted++;
$adHtml = $this->renderer->renderSlot($this->settings, 'post-content-' . $adsInserted);
$newContent .= $adHtml;
}
}

View File

@@ -105,6 +105,15 @@ final class AdsensePlacementRenderer
{
$locationKey = str_replace('-', '_', $location);
// Manejar ubicaciones de in-content (post_content_1, post_content_2, etc.)
if (preg_match('/^post_content_(\d+)$/', $locationKey, $matches)) {
// In-content ads heredan la configuracion de post_content
return [
'enabled' => $settings['behavior']['post_content_enabled'] ?? false,
'format' => $settings['behavior']['post_content_format'] ?? 'in-article',
];
}
// Mapeo de ubicaciones a grupos y campos
$locationMap = [
'post_top' => ['group' => 'behavior', 'enabled' => 'post_top_enabled', 'format' => 'post_top_format'],