Compare commits
28 Commits
b509b1a2b4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9e9561984 | ||
|
|
a2dfd10f9e | ||
|
|
5971f2c971 | ||
|
|
a4f63145dd | ||
|
|
88103a774b | ||
|
|
ffc22a21ea | ||
|
|
ed45a9c821 | ||
|
|
89a4fc5133 | ||
|
|
449e2e1740 | ||
|
|
a2c4f857be | ||
|
|
179a83e9cd | ||
|
|
555541b2a0 | ||
|
|
fae4def974 | ||
|
|
8bbbf484bd | ||
|
|
50c411408e | ||
|
|
d7c9c2a801 | ||
|
|
30068ca01e | ||
|
|
959d76fd92 | ||
|
|
04387d46bb | ||
|
|
4f1e85fe88 | ||
|
|
2cb7363cbb | ||
|
|
18bf3d191c | ||
|
|
09d87835b8 | ||
|
|
2896e2d006 | ||
|
|
c2fff49961 | ||
|
|
85f3387fd2 | ||
|
|
ff5ba25505 | ||
|
|
eab974d14c |
14
.claude/settings.local.json
Normal file
14
.claude/settings.local.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"mcp__serena__activate_project",
|
||||||
|
"mcp__serena__find_symbol",
|
||||||
|
"Bash(ssh:*)",
|
||||||
|
"Bash(php:*)",
|
||||||
|
"mcp__playwright__browser_console_messages",
|
||||||
|
"mcp__playwright__browser_evaluate",
|
||||||
|
"mcp__playwright__browser_navigate"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,11 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
|
|||||||
'adsense-placementRailFormat' => ['group' => 'behavior', 'attribute' => 'rail_format'],
|
'adsense-placementRailFormat' => ['group' => 'behavior', 'attribute' => 'rail_format'],
|
||||||
'adsense-placementRailTopOffset' => ['group' => 'behavior', 'attribute' => 'rail_top_offset'],
|
'adsense-placementRailTopOffset' => ['group' => 'behavior', 'attribute' => 'rail_top_offset'],
|
||||||
|
|
||||||
|
// BEHAVIOR (Lazy Loading)
|
||||||
|
'adsense-placementLazyLoadingEnabled' => ['group' => 'behavior', 'attribute' => 'lazy_loading_enabled'],
|
||||||
|
'adsense-placementLazyRootmargin' => ['group' => 'behavior', 'attribute' => 'lazy_rootmargin'],
|
||||||
|
'adsense-placementLazyFillTimeout' => ['group' => 'behavior', 'attribute' => 'lazy_fill_timeout'],
|
||||||
|
|
||||||
// LAYOUT (Archive/Global locations + formats)
|
// LAYOUT (Archive/Global locations + formats)
|
||||||
'adsense-placementArchiveTopEnabled' => ['group' => 'layout', 'attribute' => 'archive_top_enabled'],
|
'adsense-placementArchiveTopEnabled' => ['group' => 'layout', 'attribute' => 'archive_top_enabled'],
|
||||||
'adsense-placementArchiveBetweenEnabled' => ['group' => 'layout', 'attribute' => 'archive_between_enabled'],
|
'adsense-placementArchiveBetweenEnabled' => ['group' => 'layout', 'attribute' => 'archive_between_enabled'],
|
||||||
@@ -118,6 +123,27 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
|
|||||||
'adsense-placementExcludeCategoriesAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
'adsense-placementExcludeCategoriesAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||||
'adsense-placementExcludePostIdsAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
'adsense-placementExcludePostIdsAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||||
'adsense-placementExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
'adsense-placementExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||||
|
|
||||||
|
// INCONTENT ADVANCED (In-Content Ads Avanzado)
|
||||||
|
'adsense-placementIncontentMode' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_mode'],
|
||||||
|
'adsense-placementIncontentAfterH2Enabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h2_enabled'],
|
||||||
|
'adsense-placementIncontentAfterH2Probability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h2_probability'],
|
||||||
|
'adsense-placementIncontentAfterH3Enabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h3_enabled'],
|
||||||
|
'adsense-placementIncontentAfterH3Probability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_h3_probability'],
|
||||||
|
'adsense-placementIncontentAfterParagraphsEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_paragraphs_enabled'],
|
||||||
|
'adsense-placementIncontentAfterParagraphsProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_paragraphs_probability'],
|
||||||
|
'adsense-placementIncontentAfterImagesEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_images_enabled'],
|
||||||
|
'adsense-placementIncontentAfterImagesProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_images_probability'],
|
||||||
|
'adsense-placementIncontentAfterListsEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_lists_enabled'],
|
||||||
|
'adsense-placementIncontentAfterListsProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_lists_probability'],
|
||||||
|
'adsense-placementIncontentAfterBlockquotesEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_blockquotes_enabled'],
|
||||||
|
'adsense-placementIncontentAfterBlockquotesProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_blockquotes_probability'],
|
||||||
|
'adsense-placementIncontentAfterTablesEnabled' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_tables_enabled'],
|
||||||
|
'adsense-placementIncontentAfterTablesProbability' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_after_tables_probability'],
|
||||||
|
'adsense-placementIncontentMaxTotalAds' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_max_total_ads'],
|
||||||
|
'adsense-placementIncontentMinSpacing' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_min_spacing'],
|
||||||
|
'adsense-placementIncontentFormat' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_format'],
|
||||||
|
'adsense-placementIncontentPriorityMode' => ['group' => 'incontent_advanced', 'attribute' => 'incontent_priority_mode'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ final class AdsensePlacementFormBuilder
|
|||||||
$html .= $this->buildVisibilityGroup($componentId);
|
$html .= $this->buildVisibilityGroup($componentId);
|
||||||
$html .= $this->buildDiagramSection();
|
$html .= $this->buildDiagramSection();
|
||||||
$html .= $this->buildPostLocationsGroup($componentId);
|
$html .= $this->buildPostLocationsGroup($componentId);
|
||||||
|
$html .= $this->buildInContentAdvancedGroup($componentId);
|
||||||
$html .= $this->buildInContentAdsGroup($componentId);
|
$html .= $this->buildInContentAdsGroup($componentId);
|
||||||
$html .= $this->buildExclusionsGroup($componentId);
|
$html .= $this->buildExclusionsGroup($componentId);
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
@@ -342,6 +343,291 @@ final class AdsensePlacementFormBuilder
|
|||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seccion avanzada para In-Content Ads con multiples tipos de ubicacion
|
||||||
|
* Incluye: modo de densidad, ubicaciones por elemento, limites y espaciado
|
||||||
|
*/
|
||||||
|
private function buildInContentAdvancedGroup(string $cid): string
|
||||||
|
{
|
||||||
|
// Obtener valores actuales del grupo incontent_advanced
|
||||||
|
$mode = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_mode', 'paragraphs_only');
|
||||||
|
$mode = is_string($mode) ? $mode : 'paragraphs_only';
|
||||||
|
|
||||||
|
// Ubicaciones
|
||||||
|
$h2Enabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h2_enabled', true);
|
||||||
|
$h2Prob = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h2_probability', '100');
|
||||||
|
$h3Enabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h3_enabled', true);
|
||||||
|
$h3Prob = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_h3_probability', '50');
|
||||||
|
$paragraphsEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_paragraphs_enabled', true);
|
||||||
|
$paragraphsProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_paragraphs_probability', '75');
|
||||||
|
$imagesEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_images_enabled', true);
|
||||||
|
$imagesProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_images_probability', '75');
|
||||||
|
$listsEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_lists_enabled', false);
|
||||||
|
$listsProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_lists_probability', '50');
|
||||||
|
$blockquotesEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_blockquotes_enabled', false);
|
||||||
|
$blockquotesProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_blockquotes_probability', '50');
|
||||||
|
$tablesEnabled = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_tables_enabled', false);
|
||||||
|
$tablesProb = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_after_tables_probability', '50');
|
||||||
|
|
||||||
|
// Limites
|
||||||
|
$maxAds = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_max_total_ads', '8');
|
||||||
|
$minSpacing = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_min_spacing', '3');
|
||||||
|
$format = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_format', 'in-article');
|
||||||
|
$priorityMode = $this->renderer->getFieldValue($cid, 'incontent_advanced', 'incontent_priority_mode', 'position');
|
||||||
|
|
||||||
|
// Cast to string where needed
|
||||||
|
$h2Prob = is_string($h2Prob) ? $h2Prob : '100';
|
||||||
|
$h3Prob = is_string($h3Prob) ? $h3Prob : '50';
|
||||||
|
$paragraphsProb = is_string($paragraphsProb) ? $paragraphsProb : '75';
|
||||||
|
$imagesProb = is_string($imagesProb) ? $imagesProb : '75';
|
||||||
|
$listsProb = is_string($listsProb) ? $listsProb : '50';
|
||||||
|
$blockquotesProb = is_string($blockquotesProb) ? $blockquotesProb : '50';
|
||||||
|
$tablesProb = is_string($tablesProb) ? $tablesProb : '50';
|
||||||
|
$maxAds = is_string($maxAds) ? $maxAds : '8';
|
||||||
|
$minSpacing = is_string($minSpacing) ? $minSpacing : '3';
|
||||||
|
$format = is_string($format) ? $format : 'in-article';
|
||||||
|
$priorityMode = is_string($priorityMode) ? $priorityMode : 'position';
|
||||||
|
|
||||||
|
$isParagraphsOnly = $mode === 'paragraphs_only';
|
||||||
|
$disabledAttr = $isParagraphsOnly ? 'disabled' : '';
|
||||||
|
|
||||||
|
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #198754;">';
|
||||||
|
$html .= ' <div class="card-body">';
|
||||||
|
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||||
|
$html .= ' <i class="bi bi-body-text me-2" style="color: #198754;"></i>';
|
||||||
|
$html .= ' In-Content Ads Avanzado';
|
||||||
|
$html .= ' <span class="badge bg-success ms-2">Nuevo</span>';
|
||||||
|
$html .= ' </h5>';
|
||||||
|
|
||||||
|
// Indicador de densidad
|
||||||
|
$html .= ' <div id="roiIncontentDensityIndicator" class="alert alert-info small mb-3">';
|
||||||
|
$html .= ' <i class="bi bi-speedometer2 me-1"></i>';
|
||||||
|
$html .= ' Densidad estimada: <strong id="roiDensityLevel">Calculando...</strong>';
|
||||||
|
$html .= ' <span id="roiDensityBadge" class="badge bg-secondary ms-1">~? ads</span>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Banner informativo para modo Solo parrafos
|
||||||
|
$html .= ' <div id="roiParagraphsOnlyBanner" class="alert alert-light border small mb-3' . ($isParagraphsOnly ? '' : ' d-none') . '">';
|
||||||
|
$html .= ' <i class="bi bi-info-circle me-1 text-primary"></i>';
|
||||||
|
$html .= ' <strong>Solo parrafos:</strong> Los anuncios se insertan unicamente despues de parrafos, ';
|
||||||
|
$html .= ' usando la configuracion de la seccion "Post Content". Cambia a otro modo para elegir ubicaciones adicionales.';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Selector de modo con descripciones
|
||||||
|
$html .= ' <div class="mb-4">';
|
||||||
|
$html .= ' <label for="' . esc_attr($cid) . 'IncontentMode" class="form-label fw-semibold">';
|
||||||
|
$html .= ' <i class="bi bi-sliders me-1" style="color: #FF8600;"></i>Estrategia de insercion';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' <p class="text-muted small mb-2">Define donde y con que frecuencia se insertaran anuncios dentro del contenido.</p>';
|
||||||
|
$html .= ' <select class="form-select mb-3" id="' . esc_attr($cid) . 'IncontentMode">';
|
||||||
|
$modeOptions = [
|
||||||
|
'paragraphs_only' => 'Solo parrafos (clasico)',
|
||||||
|
'conservative' => 'Conservador - H2 y parrafos',
|
||||||
|
'balanced' => 'Balanceado - Multiples elementos',
|
||||||
|
'aggressive' => 'Intensivo - Todos los elementos',
|
||||||
|
'custom' => 'Personalizado'
|
||||||
|
];
|
||||||
|
foreach ($modeOptions as $value => $label) {
|
||||||
|
$selected = selected($mode, $value, false);
|
||||||
|
$html .= '<option value="' . esc_attr($value) . '" ' . $selected . '>' . esc_html($label) . '</option>';
|
||||||
|
}
|
||||||
|
$html .= ' </select>';
|
||||||
|
|
||||||
|
// Descripciones de cada modo
|
||||||
|
$html .= ' <div id="roiModeDescriptions" class="small">';
|
||||||
|
|
||||||
|
// Solo parrafos
|
||||||
|
$html .= ' <div id="roiModeDescParagraphsOnly" class="alert alert-light border py-2 px-3' . ($mode !== 'paragraphs_only' ? ' d-none' : '') . '">';
|
||||||
|
$html .= ' <strong class="text-primary"><i class="bi bi-text-paragraph me-1"></i>Solo parrafos</strong>';
|
||||||
|
$html .= ' <p class="mb-1 mt-1">Inserta anuncios unicamente despues de parrafos. Usa la configuracion de la seccion "Post Content" (numero de anuncios, parrafos entre ads, etc).</p>';
|
||||||
|
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Tu contenido tiene pocos encabezados o prefieres la configuracion tradicional.</span>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Conservador
|
||||||
|
$html .= ' <div id="roiModeDescConservative" class="alert alert-light border py-2 px-3' . ($mode !== 'conservative' ? ' d-none' : '') . '">';
|
||||||
|
$html .= ' <strong class="text-success"><i class="bi bi-shield-check me-1"></i>Conservador</strong>';
|
||||||
|
$html .= ' <p class="mb-1 mt-1">Maximo 5 anuncios con espaciado amplio (5 elementos). Solo inserta despues de titulos H2 y parrafos.</p>';
|
||||||
|
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Priorizas la experiencia del usuario sobre los ingresos. Articulos cortos o medianos.</span>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Balanceado
|
||||||
|
$html .= ' <div id="roiModeDescBalanced" class="alert alert-light border py-2 px-3' . ($mode !== 'balanced' ? ' d-none' : '') . '">';
|
||||||
|
$html .= ' <strong class="text-primary"><i class="bi bi-balance-scale me-1"></i>Balanceado</strong>';
|
||||||
|
$html .= ' <p class="mb-1 mt-1">Hasta 8 anuncios con espaciado moderado (3 elementos). Usa H2, H3, parrafos e imagenes.</p>';
|
||||||
|
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Buscas equilibrio entre ingresos y experiencia. Articulos medianos a largos.</span>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Intensivo
|
||||||
|
$html .= ' <div id="roiModeDescAggressive" class="alert alert-light border py-2 px-3' . ($mode !== 'aggressive' ? ' d-none' : '') . '">';
|
||||||
|
$html .= ' <strong class="text-warning"><i class="bi bi-lightning-charge me-1"></i>Intensivo</strong>';
|
||||||
|
$html .= ' <p class="mb-1 mt-1">Hasta 15 anuncios con espaciado minimo (2 elementos). Usa todos los tipos de elementos disponibles.</p>';
|
||||||
|
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Priorizas maximizar ingresos. Solo para articulos muy largos (+3000 palabras).</span>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Personalizado
|
||||||
|
$html .= ' <div id="roiModeDescCustom" class="alert alert-light border py-2 px-3' . ($mode !== 'custom' ? ' d-none' : '') . '">';
|
||||||
|
$html .= ' <strong class="text-secondary"><i class="bi bi-gear me-1"></i>Personalizado</strong>';
|
||||||
|
$html .= ' <p class="mb-1 mt-1">Tu configuras manualmente cada ubicacion, probabilidad y limites.</p>';
|
||||||
|
$html .= ' <span class="text-muted"><i class="bi bi-lightbulb me-1"></i>Ideal si: Quieres control total sobre donde aparecen los anuncios.</span>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Subseccion: Ubicaciones por elemento
|
||||||
|
$html .= ' <details class="mb-3 border rounded" id="roiLocationsDetails"' . ($isParagraphsOnly ? '' : ' open') . '>';
|
||||||
|
$html .= ' <summary class="p-3 bg-light fw-bold" style="cursor: pointer;">';
|
||||||
|
$html .= ' <i class="bi bi-geo-alt me-1"></i>';
|
||||||
|
$html .= ' Ubicaciones por Elemento';
|
||||||
|
$html .= ' </summary>';
|
||||||
|
$html .= ' <div class="p-3">';
|
||||||
|
|
||||||
|
// Grid de ubicaciones
|
||||||
|
$locations = [
|
||||||
|
['id' => 'H2', 'label' => 'Despues de H2 (titulos)', 'enabled' => $h2Enabled, 'prob' => $h2Prob, 'icon' => 'bi-type-h2'],
|
||||||
|
['id' => 'H3', 'label' => 'Despues de H3 (subtitulos)', 'enabled' => $h3Enabled, 'prob' => $h3Prob, 'icon' => 'bi-type-h3'],
|
||||||
|
['id' => 'Paragraphs', 'label' => 'Despues de parrafos', 'enabled' => $paragraphsEnabled, 'prob' => $paragraphsProb, 'icon' => 'bi-text-paragraph'],
|
||||||
|
['id' => 'Images', 'label' => 'Despues de imagenes', 'enabled' => $imagesEnabled, 'prob' => $imagesProb, 'icon' => 'bi-image'],
|
||||||
|
['id' => 'Lists', 'label' => 'Despues de listas', 'enabled' => $listsEnabled, 'prob' => $listsProb, 'icon' => 'bi-list-ul'],
|
||||||
|
['id' => 'Blockquotes', 'label' => 'Despues de citas', 'enabled' => $blockquotesEnabled, 'prob' => $blockquotesProb, 'icon' => 'bi-quote'],
|
||||||
|
['id' => 'Tables', 'label' => 'Despues de tablas', 'enabled' => $tablesEnabled, 'prob' => $tablesProb, 'icon' => 'bi-table'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$probOptions = [
|
||||||
|
'100' => '100%',
|
||||||
|
'75' => '75%',
|
||||||
|
'50' => '50%',
|
||||||
|
'25' => '25%'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($locations as $loc) {
|
||||||
|
$enabledId = $cid . 'IncontentAfter' . $loc['id'] . 'Enabled';
|
||||||
|
$probId = $cid . 'IncontentAfter' . $loc['id'] . 'Probability';
|
||||||
|
$checked = checked($loc['enabled'], true, false);
|
||||||
|
|
||||||
|
$html .= ' <div class="row g-2 mb-2 align-items-center">';
|
||||||
|
$html .= ' <div class="col-7">';
|
||||||
|
$html .= ' <div class="form-check form-switch">';
|
||||||
|
$html .= ' <input type="checkbox" class="form-check-input roi-incontent-location" ';
|
||||||
|
$html .= ' id="' . esc_attr($enabledId) . '" ' . $checked . ' ' . $disabledAttr . '>';
|
||||||
|
$html .= ' <label class="form-check-label small" for="' . esc_attr($enabledId) . '">';
|
||||||
|
$html .= ' <i class="bi ' . esc_attr($loc['icon']) . ' me-1" style="color: #0d6efd;"></i>';
|
||||||
|
$html .= ' ' . esc_html($loc['label']);
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-5">';
|
||||||
|
$html .= ' <select class="form-select form-select-sm roi-incontent-prob" ';
|
||||||
|
$html .= ' id="' . esc_attr($probId) . '" ' . $disabledAttr . '>';
|
||||||
|
foreach ($probOptions as $pValue => $pLabel) {
|
||||||
|
$pSelected = selected($loc['prob'], $pValue, false);
|
||||||
|
$html .= '<option value="' . esc_attr($pValue) . '" ' . $pSelected . '>' . esc_html($pLabel) . '</option>';
|
||||||
|
}
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </details>';
|
||||||
|
|
||||||
|
// Subseccion: Limites y espaciado
|
||||||
|
$html .= ' <details class="mb-3 border rounded" id="roiLimitsDetails"' . ($isParagraphsOnly ? '' : ' open') . '>';
|
||||||
|
$html .= ' <summary class="p-3 bg-light fw-bold" style="cursor: pointer;">';
|
||||||
|
$html .= ' <i class="bi bi-sliders me-1"></i>';
|
||||||
|
$html .= ' Limites y Espaciado';
|
||||||
|
$html .= ' </summary>';
|
||||||
|
$html .= ' <div class="p-3">';
|
||||||
|
$html .= ' <div class="row g-3">';
|
||||||
|
|
||||||
|
// Max total ads
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$html .= ' <label for="' . esc_attr($cid) . 'IncontentMaxTotalAds" class="form-label small fw-semibold">';
|
||||||
|
$html .= ' Maximo total de ads';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($cid) . 'IncontentMaxTotalAds" ' . $disabledAttr . '>';
|
||||||
|
for ($i = 1; $i <= 25; $i++) {
|
||||||
|
$iStr = (string)$i;
|
||||||
|
$adSelected = selected($maxAds, $iStr, false);
|
||||||
|
$label = $i === 1 ? '1 anuncio' : $i . ' anuncios';
|
||||||
|
$html .= '<option value="' . esc_attr($iStr) . '" ' . $adSelected . '>' . esc_html($label) . '</option>';
|
||||||
|
}
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Min spacing
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$html .= ' <label for="' . esc_attr($cid) . 'IncontentMinSpacing" class="form-label small fw-semibold">';
|
||||||
|
$html .= ' Espaciado minimo (elementos)';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($cid) . 'IncontentMinSpacing" ' . $disabledAttr . '>';
|
||||||
|
$spacingOptions = [
|
||||||
|
'1' => '1 elemento',
|
||||||
|
'2' => '2 elementos',
|
||||||
|
'3' => '3 elementos',
|
||||||
|
'4' => '4 elementos',
|
||||||
|
'5' => '5 elementos',
|
||||||
|
'6' => '6 elementos'
|
||||||
|
];
|
||||||
|
foreach ($spacingOptions as $sValue => $sLabel) {
|
||||||
|
$sSelected = selected($minSpacing, $sValue, false);
|
||||||
|
$html .= '<option value="' . esc_attr($sValue) . '" ' . $sSelected . '>' . esc_html($sLabel) . '</option>';
|
||||||
|
}
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Formato de ads
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$html .= ' <label for="' . esc_attr($cid) . 'IncontentFormat" class="form-label small fw-semibold">';
|
||||||
|
$html .= ' Formato de ads';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($cid) . 'IncontentFormat" ' . $disabledAttr . '>';
|
||||||
|
$formatOptions = [
|
||||||
|
'in-article' => 'In-Article (fluid)',
|
||||||
|
'auto' => 'Auto (responsive)'
|
||||||
|
];
|
||||||
|
foreach ($formatOptions as $fValue => $fLabel) {
|
||||||
|
$fSelected = selected($format, $fValue, false);
|
||||||
|
$html .= '<option value="' . esc_attr($fValue) . '" ' . $fSelected . '>' . esc_html($fLabel) . '</option>';
|
||||||
|
}
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
// Priority mode
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$html .= ' <label for="' . esc_attr($cid) . 'IncontentPriorityMode" class="form-label small fw-semibold">';
|
||||||
|
$html .= ' Estrategia de seleccion';
|
||||||
|
$html .= ' </label>';
|
||||||
|
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($cid) . 'IncontentPriorityMode" ' . $disabledAttr . '>';
|
||||||
|
$priorityOptions = [
|
||||||
|
'position' => 'Por posicion (distribucion uniforme)',
|
||||||
|
'priority' => 'Por prioridad (maximizar H2/H3)'
|
||||||
|
];
|
||||||
|
foreach ($priorityOptions as $pmValue => $pmLabel) {
|
||||||
|
$pmSelected = selected($priorityMode, $pmValue, false);
|
||||||
|
$html .= '<option value="' . esc_attr($pmValue) . '" ' . $pmSelected . '>' . esc_html($pmLabel) . '</option>';
|
||||||
|
}
|
||||||
|
$html .= ' </select>';
|
||||||
|
$html .= ' <small class="text-muted">Como resolver conflictos cuando dos ubicaciones estan muy cerca</small>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' </details>';
|
||||||
|
|
||||||
|
// Warning para densidad alta
|
||||||
|
$html .= ' <div id="roiHighDensityWarning" class="alert alert-warning small d-none">';
|
||||||
|
$html .= ' <i class="bi bi-exclamation-triangle me-1"></i>';
|
||||||
|
$html .= ' <strong>Atencion:</strong> Densidad alta (>10 ads) puede afectar UX y violar politicas de AdSense.';
|
||||||
|
$html .= ' </div>';
|
||||||
|
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seccion especial para in-content ads con configuracion de 1-8 random
|
* Seccion especial para in-content ads con configuracion de 1-8 random
|
||||||
*/
|
*/
|
||||||
@@ -896,6 +1182,55 @@ final class AdsensePlacementFormBuilder
|
|||||||
$delayTimeout = $this->renderer->getFieldValue($cid, 'forms', 'delay_timeout', '5000');
|
$delayTimeout = $this->renderer->getFieldValue($cid, 'forms', 'delay_timeout', '5000');
|
||||||
$html .= $this->buildTextInput($cid . 'DelayTimeout', 'Timeout de delay (ms)', $delayTimeout);
|
$html .= $this->buildTextInput($cid . 'DelayTimeout', 'Timeout de delay (ms)', $delayTimeout);
|
||||||
|
|
||||||
|
// Lazy Loading settings
|
||||||
|
$html .= '<hr class="my-3">';
|
||||||
|
$html .= '<p class="small fw-semibold mb-2">';
|
||||||
|
$html .= ' <i class="bi bi-eye me-1" style="color: #198754;"></i>';
|
||||||
|
$html .= ' Lazy Loading (Intersection Observer)';
|
||||||
|
$html .= ' <span class="badge bg-success ms-1">Nuevo</span>';
|
||||||
|
$html .= '</p>';
|
||||||
|
$html .= '<div class="alert alert-info small py-2 mb-2">';
|
||||||
|
$html .= ' <i class="bi bi-lightbulb me-1"></i>';
|
||||||
|
$html .= ' Carga anuncios individualmente al entrar al viewport. Mejora fill rate y reduce CLS.';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$lazyEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'lazy_loading_enabled', true);
|
||||||
|
$html .= $this->buildSwitch($cid . 'LazyLoadingEnabled', 'Activar Lazy Loading', $lazyEnabled, 'bi-eye');
|
||||||
|
|
||||||
|
$html .= '<div class="row g-2">';
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$lazyRootmargin = $this->renderer->getFieldValue($cid, 'behavior', 'lazy_rootmargin', '200');
|
||||||
|
$html .= $this->buildSelect($cid . 'LazyRootmargin', 'Pre-carga (px)',
|
||||||
|
(string)$lazyRootmargin,
|
||||||
|
[
|
||||||
|
'0' => '0px (sin pre-carga)',
|
||||||
|
'100' => '100px',
|
||||||
|
'200' => '200px (recomendado)',
|
||||||
|
'300' => '300px',
|
||||||
|
'400' => '400px',
|
||||||
|
'500' => '500px'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= ' <div class="col-md-6">';
|
||||||
|
$lazyFillTimeout = $this->renderer->getFieldValue($cid, 'behavior', 'lazy_fill_timeout', '5000');
|
||||||
|
$html .= $this->buildSelect($cid . 'LazyFillTimeout', 'Timeout fill (ms)',
|
||||||
|
(string)$lazyFillTimeout,
|
||||||
|
[
|
||||||
|
'3000' => '3 segundos',
|
||||||
|
'5000' => '5 segundos (recomendado)',
|
||||||
|
'7000' => '7 segundos',
|
||||||
|
'10000' => '10 segundos'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$html .= ' </div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '<div class="alert alert-warning small py-2 mt-2 mb-0">';
|
||||||
|
$html .= ' <i class="bi bi-exclamation-triangle me-1"></i>';
|
||||||
|
$html .= ' <strong>Nota:</strong> Cambios requieren vaciar cache (Redis, W3TC) para aplicarse.';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
$html .= ' </div>';
|
$html .= ' </div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
|
|||||||
@@ -518,4 +518,402 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// IN-CONTENT ADS AVANZADO - JavaScript
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initializeInContentAdvanced();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicializa la funcionalidad de In-Content Ads Avanzado
|
||||||
|
*/
|
||||||
|
function initializeInContentAdvanced() {
|
||||||
|
// Buscar el selector de modo (puede tener prefijo dinámico)
|
||||||
|
const modeSelect = document.querySelector('[id$="IncontentMode"]');
|
||||||
|
if (!modeSelect) {
|
||||||
|
return; // No estamos en la página de AdSense
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener prefijo del componente desde el ID
|
||||||
|
const componentPrefix = modeSelect.id.replace('IncontentMode', '');
|
||||||
|
|
||||||
|
// Definir presets de modos
|
||||||
|
const modePresets = {
|
||||||
|
'paragraphs_only': null, // Solo inserta despues de parrafos (config basica)
|
||||||
|
'conservative': {
|
||||||
|
maxAds: '5',
|
||||||
|
minSpacing: '5',
|
||||||
|
h2: { enabled: true, prob: '75' },
|
||||||
|
h3: { enabled: false, prob: '50' },
|
||||||
|
paragraphs: { enabled: true, prob: '50' },
|
||||||
|
images: { enabled: false, prob: '50' },
|
||||||
|
lists: { enabled: false, prob: '50' },
|
||||||
|
blockquotes: { enabled: false, prob: '50' },
|
||||||
|
tables: { enabled: false, prob: '50' }
|
||||||
|
},
|
||||||
|
'balanced': {
|
||||||
|
maxAds: '8',
|
||||||
|
minSpacing: '3',
|
||||||
|
h2: { enabled: true, prob: '100' },
|
||||||
|
h3: { enabled: true, prob: '50' },
|
||||||
|
paragraphs: { enabled: true, prob: '75' },
|
||||||
|
images: { enabled: true, prob: '75' },
|
||||||
|
lists: { enabled: false, prob: '50' },
|
||||||
|
blockquotes: { enabled: false, prob: '50' },
|
||||||
|
tables: { enabled: false, prob: '50' }
|
||||||
|
},
|
||||||
|
'aggressive': {
|
||||||
|
maxAds: '15',
|
||||||
|
minSpacing: '2',
|
||||||
|
h2: { enabled: true, prob: '100' },
|
||||||
|
h3: { enabled: true, prob: '100' },
|
||||||
|
paragraphs: { enabled: true, prob: '100' },
|
||||||
|
images: { enabled: true, prob: '100' },
|
||||||
|
lists: { enabled: true, prob: '75' },
|
||||||
|
blockquotes: { enabled: true, prob: '75' },
|
||||||
|
tables: { enabled: true, prob: '75' }
|
||||||
|
},
|
||||||
|
'custom': null // Configuración manual
|
||||||
|
};
|
||||||
|
|
||||||
|
// Elementos del DOM
|
||||||
|
const elements = {
|
||||||
|
mode: modeSelect,
|
||||||
|
paragraphsOnlyBanner: document.getElementById('roiParagraphsOnlyBanner'),
|
||||||
|
densityIndicator: document.getElementById('roiIncontentDensityIndicator'),
|
||||||
|
densityLevel: document.getElementById('roiDensityLevel'),
|
||||||
|
densityBadge: document.getElementById('roiDensityBadge'),
|
||||||
|
highDensityWarning: document.getElementById('roiHighDensityWarning'),
|
||||||
|
locationsDetails: document.getElementById('roiLocationsDetails'),
|
||||||
|
limitsDetails: document.getElementById('roiLimitsDetails'),
|
||||||
|
maxAds: document.querySelector('[id$="IncontentMaxTotalAds"]'),
|
||||||
|
minSpacing: document.querySelector('[id$="IncontentMinSpacing"]'),
|
||||||
|
// Descripciones de modos
|
||||||
|
modeDescriptions: {
|
||||||
|
paragraphs_only: document.getElementById('roiModeDescParagraphsOnly'),
|
||||||
|
conservative: document.getElementById('roiModeDescConservative'),
|
||||||
|
balanced: document.getElementById('roiModeDescBalanced'),
|
||||||
|
aggressive: document.getElementById('roiModeDescAggressive'),
|
||||||
|
custom: document.getElementById('roiModeDescCustom')
|
||||||
|
},
|
||||||
|
locations: [
|
||||||
|
{ key: 'H2', el: document.querySelector('[id$="IncontentAfterH2Enabled"]'), prob: document.querySelector('[id$="IncontentAfterH2Probability"]') },
|
||||||
|
{ key: 'H3', el: document.querySelector('[id$="IncontentAfterH3Enabled"]'), prob: document.querySelector('[id$="IncontentAfterH3Probability"]') },
|
||||||
|
{ key: 'Paragraphs', el: document.querySelector('[id$="IncontentAfterParagraphsEnabled"]'), prob: document.querySelector('[id$="IncontentAfterParagraphsProbability"]') },
|
||||||
|
{ key: 'Images', el: document.querySelector('[id$="IncontentAfterImagesEnabled"]'), prob: document.querySelector('[id$="IncontentAfterImagesProbability"]') },
|
||||||
|
{ key: 'Lists', el: document.querySelector('[id$="IncontentAfterListsEnabled"]'), prob: document.querySelector('[id$="IncontentAfterListsProbability"]') },
|
||||||
|
{ key: 'Blockquotes', el: document.querySelector('[id$="IncontentAfterBlockquotesEnabled"]'), prob: document.querySelector('[id$="IncontentAfterBlockquotesProbability"]') },
|
||||||
|
{ key: 'Tables', el: document.querySelector('[id$="IncontentAfterTablesEnabled"]'), prob: document.querySelector('[id$="IncontentAfterTablesProbability"]') }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estado para detectar cambios manuales
|
||||||
|
let isApplyingPreset = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualiza el indicador de densidad
|
||||||
|
*/
|
||||||
|
function updateDensityIndicator() {
|
||||||
|
const mode = elements.mode.value;
|
||||||
|
|
||||||
|
if (mode === 'paragraphs_only') {
|
||||||
|
elements.densityLevel.textContent = 'Solo parrafos';
|
||||||
|
elements.densityBadge.textContent = 'clasico';
|
||||||
|
elements.densityBadge.className = 'badge bg-secondary ms-1';
|
||||||
|
elements.densityIndicator.className = 'alert alert-light border small mb-3';
|
||||||
|
elements.highDensityWarning.classList.add('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular densidad estimada
|
||||||
|
const maxAds = parseInt(elements.maxAds.value) || 8;
|
||||||
|
let totalWeight = 0;
|
||||||
|
let enabledCount = 0;
|
||||||
|
|
||||||
|
elements.locations.forEach(loc => {
|
||||||
|
if (loc.el && loc.el.checked) {
|
||||||
|
const prob = parseInt(loc.prob.value) || 100;
|
||||||
|
totalWeight += prob;
|
||||||
|
enabledCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const avgProb = enabledCount > 0 ? totalWeight / enabledCount : 0;
|
||||||
|
const estimatedAds = Math.round((maxAds * avgProb) / 100);
|
||||||
|
|
||||||
|
// Determinar nivel
|
||||||
|
let level, badgeClass, alertClass;
|
||||||
|
if (estimatedAds <= 3) {
|
||||||
|
level = 'Baja';
|
||||||
|
badgeClass = 'bg-success';
|
||||||
|
alertClass = 'alert-success';
|
||||||
|
} else if (estimatedAds <= 6) {
|
||||||
|
level = 'Media';
|
||||||
|
badgeClass = 'bg-info';
|
||||||
|
alertClass = 'alert-info';
|
||||||
|
} else if (estimatedAds <= 10) {
|
||||||
|
level = 'Alta';
|
||||||
|
badgeClass = 'bg-warning';
|
||||||
|
alertClass = 'alert-warning';
|
||||||
|
} else {
|
||||||
|
level = 'Muy Alta';
|
||||||
|
badgeClass = 'bg-danger';
|
||||||
|
alertClass = 'alert-danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.densityLevel.textContent = level;
|
||||||
|
elements.densityBadge.textContent = '~' + estimatedAds + ' ads';
|
||||||
|
elements.densityBadge.className = 'badge ' + badgeClass + ' ms-1';
|
||||||
|
elements.densityIndicator.className = 'alert ' + alertClass + ' small mb-3';
|
||||||
|
|
||||||
|
// Mostrar/ocultar warning de densidad alta
|
||||||
|
if (estimatedAds > 10) {
|
||||||
|
elements.highDensityWarning.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
elements.highDensityWarning.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aplica un preset de modo
|
||||||
|
*/
|
||||||
|
function applyPreset(presetName) {
|
||||||
|
const preset = modePresets[presetName];
|
||||||
|
if (!preset) return;
|
||||||
|
|
||||||
|
isApplyingPreset = true;
|
||||||
|
|
||||||
|
// Aplicar max ads y spacing
|
||||||
|
if (elements.maxAds) elements.maxAds.value = preset.maxAds;
|
||||||
|
if (elements.minSpacing) elements.minSpacing.value = preset.minSpacing;
|
||||||
|
|
||||||
|
// Aplicar ubicaciones
|
||||||
|
const locationKeys = ['h2', 'h3', 'paragraphs', 'images', 'lists', 'blockquotes', 'tables'];
|
||||||
|
locationKeys.forEach((key, index) => {
|
||||||
|
const loc = elements.locations[index];
|
||||||
|
const presetLoc = preset[key];
|
||||||
|
if (loc.el && presetLoc) {
|
||||||
|
loc.el.checked = presetLoc.enabled;
|
||||||
|
if (loc.prob) loc.prob.value = presetLoc.prob;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
isApplyingPreset = false;
|
||||||
|
updateDensityIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Habilita/deshabilita campos según modo
|
||||||
|
*/
|
||||||
|
function toggleFieldsState() {
|
||||||
|
const currentMode = elements.mode.value;
|
||||||
|
const isParagraphsOnly = currentMode === 'paragraphs_only';
|
||||||
|
|
||||||
|
// Toggle details sections
|
||||||
|
if (elements.locationsDetails) {
|
||||||
|
if (isParagraphsOnly) {
|
||||||
|
elements.locationsDetails.removeAttribute('open');
|
||||||
|
} else {
|
||||||
|
elements.locationsDetails.setAttribute('open', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (elements.limitsDetails) {
|
||||||
|
if (isParagraphsOnly) {
|
||||||
|
elements.limitsDetails.removeAttribute('open');
|
||||||
|
} else {
|
||||||
|
elements.limitsDetails.setAttribute('open', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle campos
|
||||||
|
if (elements.maxAds) elements.maxAds.disabled = isParagraphsOnly;
|
||||||
|
if (elements.minSpacing) elements.minSpacing.disabled = isParagraphsOnly;
|
||||||
|
|
||||||
|
elements.locations.forEach(loc => {
|
||||||
|
if (loc.el) loc.el.disabled = isParagraphsOnly;
|
||||||
|
if (loc.prob) loc.prob.disabled = isParagraphsOnly;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle banner informativo
|
||||||
|
if (elements.paragraphsOnlyBanner) {
|
||||||
|
if (isParagraphsOnly) {
|
||||||
|
elements.paragraphsOnlyBanner.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
elements.paragraphsOnlyBanner.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle descripciones de modo (mostrar solo la activa)
|
||||||
|
if (elements.modeDescriptions) {
|
||||||
|
Object.keys(elements.modeDescriptions).forEach(mode => {
|
||||||
|
const descEl = elements.modeDescriptions[mode];
|
||||||
|
if (descEl) {
|
||||||
|
if (mode === currentMode) {
|
||||||
|
descEl.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
descEl.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar indicador
|
||||||
|
updateDensityIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maneja cambio de modo
|
||||||
|
*/
|
||||||
|
function handleModeChange(e) {
|
||||||
|
const newMode = e.target.value;
|
||||||
|
const currentMode = e.target.dataset.previousValue || 'paragraphs_only';
|
||||||
|
|
||||||
|
// Si cambia de custom a preset, mostrar confirmación
|
||||||
|
if (currentMode === 'custom' && newMode !== 'custom' && modePresets[newMode]) {
|
||||||
|
showConfirmModal(
|
||||||
|
'Cambiar modo',
|
||||||
|
'Al cambiar a un modo preconfigurado se perderán tus ajustes personalizados. ¿Continuar?',
|
||||||
|
function() {
|
||||||
|
applyPreset(newMode);
|
||||||
|
toggleFieldsState();
|
||||||
|
e.target.dataset.previousValue = newMode;
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
// Cancelar: restaurar valor anterior
|
||||||
|
e.target.value = currentMode;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar preset si corresponde
|
||||||
|
if (modePresets[newMode]) {
|
||||||
|
applyPreset(newMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFieldsState();
|
||||||
|
e.target.dataset.previousValue = newMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maneja cambios en campos (auto-switch a custom)
|
||||||
|
*/
|
||||||
|
function handleFieldChange() {
|
||||||
|
if (isApplyingPreset) return;
|
||||||
|
|
||||||
|
const currentMode = elements.mode.value;
|
||||||
|
if (currentMode !== 'custom' && currentMode !== 'paragraphs_only') {
|
||||||
|
elements.mode.value = 'custom';
|
||||||
|
elements.mode.dataset.previousValue = 'custom';
|
||||||
|
showNotice('info', 'Modo cambiado a "Personalizado" por tus ajustes manuales.');
|
||||||
|
updateDensityIndicator();
|
||||||
|
} else {
|
||||||
|
updateDensityIndicator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar estado
|
||||||
|
elements.mode.dataset.previousValue = elements.mode.value;
|
||||||
|
toggleFieldsState();
|
||||||
|
updateDensityIndicator();
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
elements.mode.addEventListener('change', handleModeChange);
|
||||||
|
|
||||||
|
if (elements.maxAds) {
|
||||||
|
elements.maxAds.addEventListener('change', handleFieldChange);
|
||||||
|
}
|
||||||
|
if (elements.minSpacing) {
|
||||||
|
elements.minSpacing.addEventListener('change', handleFieldChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.locations.forEach(loc => {
|
||||||
|
if (loc.el) {
|
||||||
|
loc.el.addEventListener('change', handleFieldChange);
|
||||||
|
}
|
||||||
|
if (loc.prob) {
|
||||||
|
loc.prob.addEventListener('change', handleFieldChange);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Muestra un modal de confirmación con callback de cancelación
|
||||||
|
*/
|
||||||
|
function showConfirmModal(title, message, onConfirm, onCancel) {
|
||||||
|
// Crear modal si no existe
|
||||||
|
let modal = document.getElementById('roiConfirmModal');
|
||||||
|
if (!modal) {
|
||||||
|
const modalHTML = `
|
||||||
|
<div class="modal fade" id="roiConfirmModal" tabindex="-1" aria-labelledby="roiConfirmModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-bottom: none;">
|
||||||
|
<h5 class="modal-title text-white" id="roiConfirmModalLabel">
|
||||||
|
<i class="bi bi-question-circle me-2" style="color: #FF8600;"></i>
|
||||||
|
<span id="roiConfirmModalTitle">Confirmar</span>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="roiConfirmModalBody" style="padding: 2rem;">
|
||||||
|
Mensaje de confirmación
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 1rem 1.5rem;">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn text-white" id="roiConfirmModalConfirm" style="background-color: #FF8600; border-color: #FF8600;">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>
|
||||||
|
Confirmar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||||
|
modal = document.getElementById('roiConfirmModal');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar contenido
|
||||||
|
document.getElementById('roiConfirmModalTitle').textContent = title;
|
||||||
|
document.getElementById('roiConfirmModalBody').textContent = message;
|
||||||
|
|
||||||
|
// Configurar callback de confirmación
|
||||||
|
const confirmButton = document.getElementById('roiConfirmModalConfirm');
|
||||||
|
const newConfirmButton = confirmButton.cloneNode(true);
|
||||||
|
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
|
||||||
|
|
||||||
|
newConfirmButton.addEventListener('click', function() {
|
||||||
|
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||||
|
bsModal.hide();
|
||||||
|
if (typeof onConfirm === 'function') {
|
||||||
|
onConfirm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configurar callback de cancelación
|
||||||
|
if (typeof onCancel === 'function') {
|
||||||
|
modal.addEventListener('hidden.bs.modal', function handler() {
|
||||||
|
modal.removeEventListener('hidden.bs.modal', handler);
|
||||||
|
// Solo llamar onCancel si no fue por confirmación
|
||||||
|
if (!modal.dataset.confirmed) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
delete modal.dataset.confirmed;
|
||||||
|
});
|
||||||
|
|
||||||
|
newConfirmButton.addEventListener('click', function() {
|
||||||
|
modal.dataset.confirmed = 'true';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar modal
|
||||||
|
const bsModal = new bootstrap.Modal(modal);
|
||||||
|
bsModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,278 +1,663 @@
|
|||||||
/**
|
/**
|
||||||
* Cargador Retrasado de AdSense
|
* AdSense Lazy Loader con Intersection Observer
|
||||||
*
|
*
|
||||||
* Este script retrasa la carga de Google AdSense hasta que haya interacción
|
* Carga anuncios AdSense individualmente cuando entran al viewport,
|
||||||
* del usuario o se cumpla un timeout, mejorando el rendimiento de carga inicial.
|
* detecta si reciben contenido, y oculta slots vacios.
|
||||||
*
|
*
|
||||||
* @package ROI_Theme
|
* @package ROI_Theme
|
||||||
* @since 1.0.0
|
* @since 1.5.0
|
||||||
|
* @version 2.0.0 - Refactorizado con Intersection Observer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Configuración
|
// =========================================================================
|
||||||
const CONFIG = {
|
// CONFIGURACION
|
||||||
timeout: 5000, // Timeout de fallback en milisegundos
|
// =========================================================================
|
||||||
loadedClass: 'adsense-loaded',
|
|
||||||
debug: false // Cambiar a true para logs en consola
|
/**
|
||||||
|
* Configuracion por defecto, sobrescrita por window.roiAdsenseConfig
|
||||||
|
*
|
||||||
|
* rootMargin: 600px precarga slots 600px antes de entrar al viewport.
|
||||||
|
* Esto da tiempo suficiente para que AdSense cargue el anuncio antes
|
||||||
|
* de que el usuario llegue al slot, evitando layout shift.
|
||||||
|
* Basado en best practices: https://support.google.com/adsense/answer/10762946
|
||||||
|
*/
|
||||||
|
var DEFAULT_CONFIG = {
|
||||||
|
lazyEnabled: true,
|
||||||
|
rootMargin: '600px 0px',
|
||||||
|
fillTimeout: 5000,
|
||||||
|
debug: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Estado
|
/**
|
||||||
let adsenseLoaded = false;
|
* Obtiene configuracion desde wp_localize_script o usa defaults
|
||||||
let loadTimeout = null;
|
*/
|
||||||
|
function getConfig() {
|
||||||
|
var wpConfig = window.roiAdsenseConfig || {};
|
||||||
|
return {
|
||||||
|
lazyEnabled: typeof wpConfig.lazyEnabled !== 'undefined' ? wpConfig.lazyEnabled : DEFAULT_CONFIG.lazyEnabled,
|
||||||
|
rootMargin: wpConfig.rootMargin || DEFAULT_CONFIG.rootMargin,
|
||||||
|
fillTimeout: typeof wpConfig.fillTimeout !== 'undefined' ? parseInt(wpConfig.fillTimeout, 10) : DEFAULT_CONFIG.fillTimeout,
|
||||||
|
debug: typeof wpConfig.debug !== 'undefined' ? wpConfig.debug : DEFAULT_CONFIG.debug
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var CONFIG = getConfig();
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// ESTADO GLOBAL
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
var libraryLoaded = false;
|
||||||
|
var libraryLoading = false;
|
||||||
|
var libraryLoadFailed = false;
|
||||||
|
var loadRetryCount = 0;
|
||||||
|
var MAX_LOAD_RETRIES = 1;
|
||||||
|
var RETRY_DELAY = 2000;
|
||||||
|
|
||||||
|
/** @type {IntersectionObserver|null} */
|
||||||
|
var slotObserver = null;
|
||||||
|
|
||||||
|
/** @type {Map<Element, MutationObserver>} */
|
||||||
|
var fillObservers = new Map();
|
||||||
|
|
||||||
|
/** @type {Map<Element, number>} */
|
||||||
|
var fillTimeouts = new Map();
|
||||||
|
|
||||||
|
/** @type {Set<Element>} */
|
||||||
|
var activatedSlots = new Set();
|
||||||
|
|
||||||
|
/** @type {Array<Function>} */
|
||||||
|
var pendingActivations = [];
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// LOGGING
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registra mensajes de debug si el modo debug está habilitado
|
* Log condicional basado en CONFIG.debug
|
||||||
* @param {string} message - El mensaje a registrar
|
* @param {string} message
|
||||||
|
* @param {string} [level='log'] - 'log', 'warn', 'error'
|
||||||
*/
|
*/
|
||||||
function debugLog(message) {
|
function debugLog(message, level) {
|
||||||
if (CONFIG.debug && typeof console !== 'undefined') {
|
if (!CONFIG.debug || typeof console === 'undefined') {
|
||||||
console.log('[AdSense Loader] ' + message);
|
return;
|
||||||
|
}
|
||||||
|
level = level || 'log';
|
||||||
|
var prefix = '[AdSense Lazy] ';
|
||||||
|
if (level === 'error') {
|
||||||
|
console.error(prefix + message);
|
||||||
|
} else if (level === 'warn') {
|
||||||
|
console.warn(prefix + message);
|
||||||
|
} else {
|
||||||
|
console.log(prefix + message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// DETECCION DE SOPORTE
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carga los scripts de AdSense e inicializa los ads
|
* Verifica si el navegador soporta Intersection Observer
|
||||||
*/
|
*/
|
||||||
function loadAdSense() {
|
function hasIntersectionObserverSupport() {
|
||||||
// Prevenir múltiples cargas
|
return typeof window.IntersectionObserver !== 'undefined';
|
||||||
if (adsenseLoaded) {
|
}
|
||||||
debugLog('AdSense ya fue cargado, omitiendo...');
|
|
||||||
|
/**
|
||||||
|
* Verifica si el navegador soporta MutationObserver
|
||||||
|
*/
|
||||||
|
function hasMutationObserverSupport() {
|
||||||
|
return typeof window.MutationObserver !== 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// CARGA DE BIBLIOTECA ADSENSE
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga la biblioteca adsbygoogle.js
|
||||||
|
* @param {Function} onSuccess
|
||||||
|
* @param {Function} onError
|
||||||
|
*/
|
||||||
|
function loadAdSenseLibrary(onSuccess, onError) {
|
||||||
|
if (libraryLoaded) {
|
||||||
|
debugLog('Biblioteca ya cargada');
|
||||||
|
onSuccess();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
adsenseLoaded = true;
|
if (libraryLoading) {
|
||||||
debugLog('Cargando scripts de AdSense...');
|
debugLog('Biblioteca en proceso de carga, encolando callback');
|
||||||
|
pendingActivations.push(onSuccess);
|
||||||
// Limpiar el timeout si existe
|
|
||||||
if (loadTimeout) {
|
|
||||||
clearTimeout(loadTimeout);
|
|
||||||
loadTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remover event listeners para prevenir múltiples triggers
|
|
||||||
removeEventListeners();
|
|
||||||
|
|
||||||
// Cargar etiquetas de script de AdSense
|
|
||||||
loadAdSenseScripts();
|
|
||||||
|
|
||||||
// Ejecutar scripts de push de AdSense
|
|
||||||
executeAdSensePushScripts();
|
|
||||||
|
|
||||||
// Agregar clase loaded al body
|
|
||||||
document.body.classList.add(CONFIG.loadedClass);
|
|
||||||
|
|
||||||
debugLog('Carga de AdSense completada');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encuentra y carga todas las etiquetas de script de AdSense retrasadas
|
|
||||||
*/
|
|
||||||
function loadAdSenseScripts() {
|
|
||||||
const delayedScripts = document.querySelectorAll('script[data-adsense-script]');
|
|
||||||
|
|
||||||
if (delayedScripts.length === 0) {
|
|
||||||
debugLog('No se encontraron scripts retrasados de AdSense');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog('Se encontraron ' + delayedScripts.length + ' script(s) retrasado(s) de AdSense');
|
libraryLoading = true;
|
||||||
|
debugLog('Cargando biblioteca adsbygoogle.js...');
|
||||||
|
|
||||||
delayedScripts.forEach(function(oldScript) {
|
var scriptTags = document.querySelectorAll('script[data-adsense-script]');
|
||||||
const newScript = document.createElement('script');
|
if (scriptTags.length === 0) {
|
||||||
|
debugLog('No se encontro script[data-adsense-script]', 'warn');
|
||||||
|
libraryLoading = false;
|
||||||
|
onError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Copiar atributos
|
var oldScript = scriptTags[0];
|
||||||
if (oldScript.src) {
|
var newScript = document.createElement('script');
|
||||||
newScript.src = oldScript.src;
|
newScript.src = oldScript.src;
|
||||||
}
|
|
||||||
|
|
||||||
// Establecer atributo async
|
|
||||||
newScript.async = true;
|
newScript.async = true;
|
||||||
|
|
||||||
// Copiar crossorigin si está presente
|
|
||||||
if (oldScript.getAttribute('crossorigin')) {
|
if (oldScript.getAttribute('crossorigin')) {
|
||||||
newScript.crossorigin = oldScript.getAttribute('crossorigin');
|
newScript.crossOrigin = oldScript.getAttribute('crossorigin');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reemplazar script viejo con el nuevo
|
newScript.onload = function() {
|
||||||
|
debugLog('Biblioteca cargada exitosamente');
|
||||||
|
libraryLoaded = true;
|
||||||
|
libraryLoading = false;
|
||||||
|
window.adsbygoogle = window.adsbygoogle || [];
|
||||||
|
|
||||||
|
// Ejecutar callbacks pendientes
|
||||||
|
onSuccess();
|
||||||
|
while (pendingActivations.length > 0) {
|
||||||
|
var callback = pendingActivations.shift();
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
newScript.onerror = function() {
|
||||||
|
debugLog('Error cargando biblioteca (intento ' + (loadRetryCount + 1) + ')', 'error');
|
||||||
|
libraryLoading = false;
|
||||||
|
|
||||||
|
if (loadRetryCount < MAX_LOAD_RETRIES) {
|
||||||
|
loadRetryCount++;
|
||||||
|
debugLog('Reintentando en ' + RETRY_DELAY + 'ms...');
|
||||||
|
setTimeout(function() {
|
||||||
|
loadAdSenseLibrary(onSuccess, onError);
|
||||||
|
}, RETRY_DELAY);
|
||||||
|
} else {
|
||||||
|
debugLog('Maximo de reintentos alcanzado', 'error');
|
||||||
|
libraryLoadFailed = true;
|
||||||
|
onError();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ejecuta scripts de push de AdSense retrasados
|
* Marca todos los slots como error cuando la biblioteca falla
|
||||||
*/
|
*/
|
||||||
function executeAdSensePushScripts() {
|
function markAllSlotsAsError() {
|
||||||
const delayedPushScripts = document.querySelectorAll('script[data-adsense-push]');
|
var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]');
|
||||||
|
slots.forEach(function(slot) {
|
||||||
|
slot.classList.add('roi-ad-error');
|
||||||
|
cleanupSlot(slot);
|
||||||
|
});
|
||||||
|
debugLog('Todos los slots marcados como error', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
if (delayedPushScripts.length === 0) {
|
// =========================================================================
|
||||||
debugLog('No se encontraron scripts de push retrasados de AdSense');
|
// ACTIVACION DE SLOTS
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activa un slot individual ejecutando adsbygoogle.push()
|
||||||
|
* @param {Element} slot
|
||||||
|
*/
|
||||||
|
function activateSlot(slot) {
|
||||||
|
if (activatedSlots.has(slot)) {
|
||||||
|
debugLog('Slot ya activado, omitiendo');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog('Se encontraron ' + delayedPushScripts.length + ' script(s) de push retrasado(s)');
|
if (libraryLoadFailed) {
|
||||||
|
debugLog('Biblioteca fallida, marcando slot como error');
|
||||||
|
slot.classList.add('roi-ad-error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Inicializar array adsbygoogle si no existe
|
activatedSlots.add(slot);
|
||||||
|
|
||||||
|
var doActivation = function() {
|
||||||
|
var ins = slot.querySelector('ins.adsbygoogle');
|
||||||
|
if (!ins) {
|
||||||
|
debugLog('No se encontro <ins> en slot', 'warn');
|
||||||
|
slot.classList.add('roi-ad-empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('Activando slot: ' + (ins.getAttribute('data-ad-slot') || 'unknown'));
|
||||||
|
|
||||||
|
// Ejecutar push
|
||||||
|
try {
|
||||||
window.adsbygoogle = window.adsbygoogle || [];
|
window.adsbygoogle = window.adsbygoogle || [];
|
||||||
|
window.adsbygoogle.push({});
|
||||||
|
} catch (e) {
|
||||||
|
debugLog('Error en push: ' + e.message, 'error');
|
||||||
|
slot.classList.add('roi-ad-error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
delayedPushScripts.forEach(function(oldScript) {
|
// Iniciar observacion de llenado
|
||||||
const scriptContent = oldScript.innerHTML;
|
startFillDetection(slot, ins);
|
||||||
|
};
|
||||||
|
|
||||||
// Crear y ejecutar nuevo script
|
// Si la biblioteca ya cargo, activar inmediatamente
|
||||||
const newScript = document.createElement('script');
|
if (libraryLoaded) {
|
||||||
newScript.innerHTML = scriptContent;
|
doActivation();
|
||||||
newScript.type = 'text/javascript';
|
} else {
|
||||||
|
// Cargar biblioteca y luego activar
|
||||||
|
loadAdSenseLibrary(doActivation, function() {
|
||||||
|
markAllSlotsAsError();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reemplazar script viejo con el nuevo
|
// =========================================================================
|
||||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
// DETECCION DE LLENADO
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/** @type {Map<Element, number>} */
|
||||||
|
var pollIntervals = new Map();
|
||||||
|
|
||||||
|
/** Intervalo de polling rapido en ms */
|
||||||
|
var POLL_INTERVAL = 50;
|
||||||
|
|
||||||
|
/** Maximo de intentos de polling (50ms * 60 = 3 segundos max) */
|
||||||
|
var MAX_POLL_ATTEMPTS = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia la deteccion de llenado para un slot
|
||||||
|
* @param {Element} slot
|
||||||
|
* @param {Element} ins
|
||||||
|
*/
|
||||||
|
function startFillDetection(slot, ins) {
|
||||||
|
// Verificar inmediatamente si ya tiene contenido
|
||||||
|
if (checkFillStatus(slot, ins)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estrategia: Polling rapido (50ms) para detectar data-ad-status lo antes posible.
|
||||||
|
// AdSense establece data-ad-status muy rapido despues de inyectar el iframe,
|
||||||
|
// pero MutationObserver a veces no lo detecta inmediatamente.
|
||||||
|
var pollCount = 0;
|
||||||
|
var pollId = setInterval(function() {
|
||||||
|
pollCount++;
|
||||||
|
|
||||||
|
if (checkFillStatus(slot, ins)) {
|
||||||
|
// Estado detectado, limpiar polling
|
||||||
|
clearInterval(pollId);
|
||||||
|
pollIntervals.delete(slot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si alcanzamos el maximo de intentos, marcar como vacio
|
||||||
|
if (pollCount >= MAX_POLL_ATTEMPTS) {
|
||||||
|
debugLog('Polling timeout alcanzado (' + (pollCount * POLL_INTERVAL) + 'ms)');
|
||||||
|
clearInterval(pollId);
|
||||||
|
pollIntervals.delete(slot);
|
||||||
|
markSlotEmpty(slot);
|
||||||
|
}
|
||||||
|
}, POLL_INTERVAL);
|
||||||
|
|
||||||
|
pollIntervals.set(slot, pollId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica el estado de llenado de un slot
|
||||||
|
* @param {Element} slot
|
||||||
|
* @param {Element} ins
|
||||||
|
* @returns {boolean} true si el estado fue determinado (filled o empty)
|
||||||
|
*/
|
||||||
|
function checkFillStatus(slot, ins) {
|
||||||
|
// IMPORTANTE: Solo data-ad-status es confiable para determinar el estado final.
|
||||||
|
// AdSense inyecta iframe ANTES de establecer data-ad-status, por lo que
|
||||||
|
// la presencia de iframe NO indica que el anuncio fue llenado.
|
||||||
|
|
||||||
|
var status = ins.getAttribute('data-ad-status');
|
||||||
|
|
||||||
|
// Estado definitivo: filled
|
||||||
|
if (status === 'filled') {
|
||||||
|
debugLog('Slot llenado (data-ad-status=filled)');
|
||||||
|
markSlotFilled(slot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estado definitivo: unfilled (sin anuncio disponible)
|
||||||
|
if (status === 'unfilled') {
|
||||||
|
debugLog('Slot vacio (data-ad-status=unfilled)');
|
||||||
|
markSlotEmpty(slot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no hay data-ad-status, AdSense aun no ha respondido.
|
||||||
|
// NO usar iframe como criterio porque AdSense inyecta iframe incluso para unfilled.
|
||||||
|
// El MutationObserver seguira observando hasta que data-ad-status aparezca o timeout.
|
||||||
|
debugLog('Esperando data-ad-status...');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marca un slot como llenado
|
||||||
|
* @param {Element} slot
|
||||||
|
*/
|
||||||
|
function markSlotFilled(slot) {
|
||||||
|
slot.classList.remove('roi-ad-empty', 'roi-ad-error');
|
||||||
|
slot.classList.add('roi-ad-filled');
|
||||||
|
cleanupSlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marca un slot como vacio
|
||||||
|
* @param {Element} slot
|
||||||
|
*/
|
||||||
|
function markSlotEmpty(slot) {
|
||||||
|
slot.classList.remove('roi-ad-filled', 'roi-ad-error');
|
||||||
|
slot.classList.add('roi-ad-empty');
|
||||||
|
cleanupSlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpia observadores, timeouts e intervalos de un slot
|
||||||
|
* @param {Element} slot
|
||||||
|
*/
|
||||||
|
function cleanupSlot(slot) {
|
||||||
|
// Limpiar polling interval
|
||||||
|
if (pollIntervals.has(slot)) {
|
||||||
|
clearInterval(pollIntervals.get(slot));
|
||||||
|
pollIntervals.delete(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpiar timeout (legacy)
|
||||||
|
if (fillTimeouts.has(slot)) {
|
||||||
|
clearTimeout(fillTimeouts.get(slot));
|
||||||
|
fillTimeouts.delete(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpiar MutationObserver (legacy)
|
||||||
|
if (fillObservers.has(slot)) {
|
||||||
|
fillObservers.get(slot).disconnect();
|
||||||
|
fillObservers.delete(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dejar de observar con IntersectionObserver
|
||||||
|
if (slotObserver) {
|
||||||
|
slotObserver.unobserve(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// INTERSECTION OBSERVER
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicializa el Intersection Observer para slots
|
||||||
|
*/
|
||||||
|
function initIntersectionObserver() {
|
||||||
|
if (!hasIntersectionObserverSupport()) {
|
||||||
|
debugLog('Sin soporte Intersection Observer, usando modo legacy', 'warn');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
root: null,
|
||||||
|
rootMargin: CONFIG.rootMargin,
|
||||||
|
threshold: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
slotObserver = new IntersectionObserver(function(entries) {
|
||||||
|
entries.forEach(function(entry) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
var slot = entry.target;
|
||||||
|
debugLog('Slot entro al viewport');
|
||||||
|
activateSlot(slot);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
debugLog('Intersection Observer inicializado con rootMargin: ' + CONFIG.rootMargin);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observa todos los slots lazy en la pagina
|
||||||
|
*/
|
||||||
|
function observeAllSlots() {
|
||||||
|
var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]');
|
||||||
|
debugLog('Encontrados ' + slots.length + ' slots para observar');
|
||||||
|
|
||||||
|
slots.forEach(function(slot) {
|
||||||
|
if (!activatedSlots.has(slot)) {
|
||||||
|
slotObserver.observe(slot);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manejador de eventos para interacciones del usuario
|
* Observa nuevos slots agregados dinamicamente
|
||||||
*/
|
*/
|
||||||
function handleUserInteraction() {
|
function observeNewSlots() {
|
||||||
debugLog('Interacción del usuario detectada');
|
var slots = document.querySelectorAll('.roi-ad-slot[data-ad-lazy="true"]');
|
||||||
loadAdSense();
|
var newCount = 0;
|
||||||
|
|
||||||
|
slots.forEach(function(slot) {
|
||||||
|
if (!activatedSlots.has(slot)) {
|
||||||
|
slotObserver.observe(slot);
|
||||||
|
newCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newCount > 0) {
|
||||||
|
debugLog('Agregados ' + newCount + ' nuevos slots al observer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// MODO LEGACY (FALLBACK)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variables para modo legacy
|
||||||
|
*/
|
||||||
|
var legacyLoaded = false;
|
||||||
|
var legacyTimeout = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga todos los ads en modo legacy (sin Intersection Observer)
|
||||||
|
*/
|
||||||
|
function loadAllAdsLegacy() {
|
||||||
|
if (legacyLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
legacyLoaded = true;
|
||||||
|
debugLog('Modo legacy: Cargando todos los ads');
|
||||||
|
|
||||||
|
if (legacyTimeout) {
|
||||||
|
clearTimeout(legacyTimeout);
|
||||||
|
}
|
||||||
|
removeLegacyEventListeners();
|
||||||
|
|
||||||
|
loadAdSenseLibrary(function() {
|
||||||
|
executeAllPushScripts();
|
||||||
|
}, function() {
|
||||||
|
debugLog('Error en modo legacy', 'error');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remueve todos los event listeners
|
* Ejecuta todos los scripts de push en modo legacy
|
||||||
*/
|
*/
|
||||||
function removeEventListeners() {
|
function executeAllPushScripts() {
|
||||||
window.removeEventListener('scroll', handleUserInteraction, { passive: true });
|
var pushScripts = document.querySelectorAll('script[data-adsense-push]');
|
||||||
window.removeEventListener('mousemove', handleUserInteraction, { passive: true });
|
debugLog('Ejecutando ' + pushScripts.length + ' scripts de push');
|
||||||
window.removeEventListener('touchstart', handleUserInteraction, { passive: true });
|
|
||||||
window.removeEventListener('click', handleUserInteraction, { passive: true });
|
window.adsbygoogle = window.adsbygoogle || [];
|
||||||
window.removeEventListener('keydown', handleUserInteraction, { passive: true });
|
|
||||||
|
pushScripts.forEach(function(oldScript) {
|
||||||
|
var newScript = document.createElement('script');
|
||||||
|
newScript.innerHTML = oldScript.innerHTML;
|
||||||
|
newScript.type = 'text/javascript';
|
||||||
|
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.classList.add('adsense-loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agrega event listeners para interacciones del usuario
|
* Event handler para modo legacy
|
||||||
*/
|
*/
|
||||||
function addEventListeners() {
|
function handleLegacyInteraction() {
|
||||||
debugLog('Agregando event listeners para interacción del usuario');
|
debugLog('Interaccion detectada (modo legacy)');
|
||||||
|
loadAllAdsLegacy();
|
||||||
// Evento de scroll - cargar en primer scroll
|
|
||||||
window.addEventListener('scroll', handleUserInteraction, { passive: true, once: true });
|
|
||||||
|
|
||||||
// Movimiento de mouse - cargar cuando el usuario mueve el mouse
|
|
||||||
window.addEventListener('mousemove', handleUserInteraction, { passive: true, once: true });
|
|
||||||
|
|
||||||
// Eventos táctiles - cargar en primer toque (móviles)
|
|
||||||
window.addEventListener('touchstart', handleUserInteraction, { passive: true, once: true });
|
|
||||||
|
|
||||||
// Eventos de click - cargar en primer click
|
|
||||||
window.addEventListener('click', handleUserInteraction, { passive: true, once: true });
|
|
||||||
|
|
||||||
// Eventos de teclado - cargar en primera pulsación de tecla
|
|
||||||
window.addEventListener('keydown', handleUserInteraction, { passive: true, once: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establece timeout de fallback para cargar AdSense después del tiempo especificado
|
* Agrega listeners para modo legacy
|
||||||
*/
|
*/
|
||||||
function setTimeoutFallback() {
|
function addLegacyEventListeners() {
|
||||||
debugLog('Estableciendo timeout de fallback (' + CONFIG.timeout + 'ms)');
|
window.addEventListener('scroll', handleLegacyInteraction, { passive: true, once: true });
|
||||||
|
window.addEventListener('mousemove', handleLegacyInteraction, { passive: true, once: true });
|
||||||
loadTimeout = setTimeout(function() {
|
window.addEventListener('touchstart', handleLegacyInteraction, { passive: true, once: true });
|
||||||
debugLog('Timeout alcanzado, cargando AdSense');
|
window.addEventListener('click', handleLegacyInteraction, { passive: true, once: true });
|
||||||
loadAdSense();
|
window.addEventListener('keydown', handleLegacyInteraction, { passive: true, once: true });
|
||||||
}, CONFIG.timeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activa slots de AdSense insertados dinamicamente
|
* Remueve listeners de modo legacy
|
||||||
* Escucha el evento 'roi-adsense-activate' disparado por otros scripts
|
*/
|
||||||
|
function removeLegacyEventListeners() {
|
||||||
|
window.removeEventListener('scroll', handleLegacyInteraction, { passive: true });
|
||||||
|
window.removeEventListener('mousemove', handleLegacyInteraction, { passive: true });
|
||||||
|
window.removeEventListener('touchstart', handleLegacyInteraction, { passive: true });
|
||||||
|
window.removeEventListener('click', handleLegacyInteraction, { passive: true });
|
||||||
|
window.removeEventListener('keydown', handleLegacyInteraction, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia modo legacy con listeners de interaccion
|
||||||
|
*/
|
||||||
|
function initLegacyMode() {
|
||||||
|
debugLog('Iniciando modo legacy');
|
||||||
|
addLegacyEventListeners();
|
||||||
|
|
||||||
|
legacyTimeout = setTimeout(function() {
|
||||||
|
debugLog('Timeout legacy alcanzado');
|
||||||
|
loadAllAdsLegacy();
|
||||||
|
}, CONFIG.fillTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// EVENTO DINAMICO
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configura listener para ads dinamicos
|
||||||
*/
|
*/
|
||||||
function setupDynamicAdsListener() {
|
function setupDynamicAdsListener() {
|
||||||
window.addEventListener('roi-adsense-activate', function() {
|
window.addEventListener('roi-adsense-activate', function() {
|
||||||
debugLog('Evento roi-adsense-activate recibido');
|
debugLog('Evento roi-adsense-activate recibido');
|
||||||
|
|
||||||
// Si AdSense aun no ha cargado, forzar carga ahora
|
if (CONFIG.lazyEnabled && slotObserver) {
|
||||||
if (!adsenseLoaded) {
|
observeNewSlots();
|
||||||
debugLog('AdSense no cargado, forzando carga...');
|
} else if (!legacyLoaded) {
|
||||||
loadAdSense();
|
loadAllAdsLegacy();
|
||||||
return;
|
} else {
|
||||||
|
// Ya cargado en legacy, ejecutar nuevos push
|
||||||
|
activateDynamicSlotsLegacy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdSense ya cargado - activar nuevos slots
|
|
||||||
debugLog('Activando nuevos slots dinamicos...');
|
|
||||||
activateDynamicSlots();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activa slots de AdSense que fueron insertados despues de la carga inicial
|
* Activa slots dinamicos en modo legacy
|
||||||
*/
|
*/
|
||||||
function activateDynamicSlots() {
|
function activateDynamicSlotsLegacy() {
|
||||||
// Buscar scripts de push que aun no han sido ejecutados
|
|
||||||
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
|
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
|
||||||
|
|
||||||
if (pendingPushScripts.length === 0) {
|
if (pendingPushScripts.length === 0) {
|
||||||
debugLog('No hay slots pendientes por activar');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)');
|
debugLog('Activando ' + pendingPushScripts.length + ' slots dinamicos (legacy)');
|
||||||
|
|
||||||
// Asegurar que adsbygoogle existe
|
|
||||||
window.adsbygoogle = window.adsbygoogle || [];
|
window.adsbygoogle = window.adsbygoogle || [];
|
||||||
|
|
||||||
pendingPushScripts.forEach(function(oldScript) {
|
pendingPushScripts.forEach(function(oldScript) {
|
||||||
try {
|
try {
|
||||||
// Crear nuevo script ejecutable
|
|
||||||
var newScript = document.createElement('script');
|
var newScript = document.createElement('script');
|
||||||
newScript.type = 'text/javascript';
|
newScript.type = 'text/javascript';
|
||||||
newScript.innerHTML = oldScript.innerHTML;
|
newScript.innerHTML = oldScript.innerHTML;
|
||||||
|
|
||||||
// Reemplazar el placeholder con el script real
|
|
||||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugLog('Error activando slot: ' + e.message);
|
debugLog('Error activando slot dinamico: ' + e.message, 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// INICIALIZACION
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inicializa el cargador retrasado de AdSense
|
* Inicializa el sistema
|
||||||
|
*
|
||||||
|
* ESTRATEGIA v2.2 (basada en documentacion oficial de Google):
|
||||||
|
* - Los slots NO estan ocultos inicialmente (Google puede no ejecutar requests para slots ocultos)
|
||||||
|
* - Usamos Intersection Observer con rootMargin grande (600px) para precargar
|
||||||
|
* - Google automaticamente oculta slots unfilled via CSS: ins[data-ad-status="unfilled"]
|
||||||
|
* - Nuestro CSS colapsa el contenedor .roi-ad-slot cuando el ins tiene unfilled
|
||||||
|
* - Esto funciona MEJOR que eager loading porque no satura AdSense con requests simultaneos
|
||||||
*/
|
*/
|
||||||
function init() {
|
function init() {
|
||||||
// =========================================================================
|
// Siempre configurar listener para ads dinamicos
|
||||||
// NUEVO: Siempre configurar listener para ads dinamicos
|
|
||||||
// IMPORTANTE: Esto debe ejecutarse ANTES del early return
|
|
||||||
// porque los ads dinamicos pueden necesitar activarse aunque
|
|
||||||
// el delay global este deshabilitado
|
|
||||||
// =========================================================================
|
|
||||||
setupDynamicAdsListener();
|
setupDynamicAdsListener();
|
||||||
debugLog('Listener para ads dinamicos configurado');
|
debugLog('Listener dinamico configurado');
|
||||||
|
|
||||||
// Verificar si el retardo de AdSense está habilitado
|
// Verificar si delay esta habilitado globalmente
|
||||||
if (!window.roiAdsenseDelayed) {
|
if (!window.roiAdsenseDelayed) {
|
||||||
debugLog('Retardo de AdSense no habilitado');
|
debugLog('Delay global no habilitado');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog('Inicializando cargador retrasado de AdSense');
|
debugLog('Inicializando AdSense Lazy Loader v2.2 (IO + Google Official CSS)');
|
||||||
|
debugLog('Config: lazyEnabled=' + CONFIG.lazyEnabled + ', rootMargin=' + CONFIG.rootMargin + ', fillTimeout=' + CONFIG.fillTimeout);
|
||||||
|
|
||||||
// Verificar si la página ya está interactiva o completa
|
// Decidir modo de operacion
|
||||||
|
if (!CONFIG.lazyEnabled) {
|
||||||
|
debugLog('Lazy loading deshabilitado, usando modo legacy');
|
||||||
|
initLegacyMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar soporte para Intersection Observer
|
||||||
|
if (!hasIntersectionObserverSupport()) {
|
||||||
|
debugLog('Sin soporte IO, usando modo legacy', 'warn');
|
||||||
|
initLegacyMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar Intersection Observer
|
||||||
|
if (!initIntersectionObserver()) {
|
||||||
|
debugLog('Fallo inicializando IO, usando modo legacy', 'warn');
|
||||||
|
initLegacyMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esperar a que el DOM este listo y observar slots
|
||||||
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
||||||
debugLog('Página ya cargada, iniciando listeners');
|
observeAllSlots();
|
||||||
addEventListeners();
|
|
||||||
setTimeoutFallback();
|
|
||||||
} else {
|
} else {
|
||||||
// Esperar a que el DOM esté listo
|
|
||||||
debugLog('Esperando a DOMContentLoaded');
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
debugLog('DOMContentLoaded disparado');
|
observeAllSlots();
|
||||||
addEventListeners();
|
|
||||||
setTimeoutFallback();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iniciar inicialización
|
// Iniciar
|
||||||
init();
|
init();
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
306
Assets/Js/adsense-loader.legacy.js
Normal file
306
Assets/Js/adsense-loader.legacy.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
/**
|
||||||
|
* Cargador Retrasado de AdSense
|
||||||
|
*
|
||||||
|
* Este script retrasa la carga de Google AdSense hasta que haya interacción
|
||||||
|
* del usuario o se cumpla un timeout, mejorando el rendimiento de carga inicial.
|
||||||
|
*
|
||||||
|
* @package ROI_Theme
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Configuración
|
||||||
|
const CONFIG = {
|
||||||
|
timeout: 5000, // Timeout de fallback en milisegundos
|
||||||
|
loadedClass: 'adsense-loaded',
|
||||||
|
debug: true // TEMPORAL: Habilitado para diagnóstico
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
let adsenseLoaded = false;
|
||||||
|
let loadTimeout = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra mensajes de debug si el modo debug está habilitado
|
||||||
|
* @param {string} message - El mensaje a registrar
|
||||||
|
*/
|
||||||
|
function debugLog(message) {
|
||||||
|
if (CONFIG.debug && typeof console !== 'undefined') {
|
||||||
|
console.log('[AdSense Loader] ' + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga los scripts de AdSense e inicializa los ads
|
||||||
|
*/
|
||||||
|
function loadAdSense() {
|
||||||
|
// Prevenir múltiples cargas
|
||||||
|
if (adsenseLoaded) {
|
||||||
|
debugLog('AdSense ya fue cargado, omitiendo...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
adsenseLoaded = true;
|
||||||
|
debugLog('Cargando scripts de AdSense...');
|
||||||
|
|
||||||
|
// Limpiar el timeout si existe
|
||||||
|
if (loadTimeout) {
|
||||||
|
clearTimeout(loadTimeout);
|
||||||
|
loadTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remover event listeners para prevenir múltiples triggers
|
||||||
|
removeEventListeners();
|
||||||
|
|
||||||
|
// Cargar etiquetas de script de AdSense y esperar a que cargue
|
||||||
|
// IMPORTANTE: Debe esperar a que adsbygoogle.js cargue antes de ejecutar push
|
||||||
|
loadAdSenseScripts(function() {
|
||||||
|
debugLog('Biblioteca AdSense cargada, ejecutando push scripts...');
|
||||||
|
|
||||||
|
// Ejecutar scripts de push de AdSense
|
||||||
|
executeAdSensePushScripts();
|
||||||
|
|
||||||
|
// Agregar clase loaded al body
|
||||||
|
document.body.classList.add(CONFIG.loadedClass);
|
||||||
|
|
||||||
|
debugLog('Carga de AdSense completada');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encuentra y carga todas las etiquetas de script de AdSense retrasadas
|
||||||
|
* @param {Function} callback - Función a ejecutar cuando la biblioteca cargue
|
||||||
|
*/
|
||||||
|
function loadAdSenseScripts(callback) {
|
||||||
|
const delayedScripts = document.querySelectorAll('script[data-adsense-script]');
|
||||||
|
|
||||||
|
if (delayedScripts.length === 0) {
|
||||||
|
debugLog('No se encontraron scripts retrasados de AdSense');
|
||||||
|
// Ejecutar callback de todas formas (puede haber ads sin script principal)
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('Se encontraron ' + delayedScripts.length + ' script(s) retrasado(s) de AdSense');
|
||||||
|
|
||||||
|
var scriptsLoaded = 0;
|
||||||
|
var totalScripts = delayedScripts.length;
|
||||||
|
|
||||||
|
delayedScripts.forEach(function(oldScript) {
|
||||||
|
const newScript = document.createElement('script');
|
||||||
|
|
||||||
|
// Copiar atributos
|
||||||
|
if (oldScript.src) {
|
||||||
|
newScript.src = oldScript.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establecer atributo async
|
||||||
|
newScript.async = true;
|
||||||
|
|
||||||
|
// Copiar crossorigin si está presente
|
||||||
|
if (oldScript.getAttribute('crossorigin')) {
|
||||||
|
newScript.crossorigin = oldScript.getAttribute('crossorigin');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esperar a que cargue antes de ejecutar callback
|
||||||
|
newScript.onload = function() {
|
||||||
|
scriptsLoaded++;
|
||||||
|
debugLog('Script cargado (' + scriptsLoaded + '/' + totalScripts + '): ' + newScript.src.substring(0, 50) + '...');
|
||||||
|
if (scriptsLoaded === totalScripts && typeof callback === 'function') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
newScript.onerror = function() {
|
||||||
|
scriptsLoaded++;
|
||||||
|
debugLog('Error cargando script: ' + newScript.src);
|
||||||
|
if (scriptsLoaded === totalScripts && typeof callback === 'function') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reemplazar script viejo con el nuevo
|
||||||
|
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejecuta scripts de push de AdSense retrasados
|
||||||
|
*/
|
||||||
|
function executeAdSensePushScripts() {
|
||||||
|
const delayedPushScripts = document.querySelectorAll('script[data-adsense-push]');
|
||||||
|
|
||||||
|
if (delayedPushScripts.length === 0) {
|
||||||
|
debugLog('No se encontraron scripts de push retrasados de AdSense');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('Se encontraron ' + delayedPushScripts.length + ' script(s) de push retrasado(s)');
|
||||||
|
|
||||||
|
// Inicializar array adsbygoogle si no existe
|
||||||
|
window.adsbygoogle = window.adsbygoogle || [];
|
||||||
|
|
||||||
|
delayedPushScripts.forEach(function(oldScript) {
|
||||||
|
const scriptContent = oldScript.innerHTML;
|
||||||
|
|
||||||
|
// Crear y ejecutar nuevo script
|
||||||
|
const newScript = document.createElement('script');
|
||||||
|
newScript.innerHTML = scriptContent;
|
||||||
|
newScript.type = 'text/javascript';
|
||||||
|
|
||||||
|
// Reemplazar script viejo con el nuevo
|
||||||
|
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manejador de eventos para interacciones del usuario
|
||||||
|
*/
|
||||||
|
function handleUserInteraction() {
|
||||||
|
debugLog('Interacción del usuario detectada');
|
||||||
|
loadAdSense();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remueve todos los event listeners
|
||||||
|
*/
|
||||||
|
function removeEventListeners() {
|
||||||
|
window.removeEventListener('scroll', handleUserInteraction, { passive: true });
|
||||||
|
window.removeEventListener('mousemove', handleUserInteraction, { passive: true });
|
||||||
|
window.removeEventListener('touchstart', handleUserInteraction, { passive: true });
|
||||||
|
window.removeEventListener('click', handleUserInteraction, { passive: true });
|
||||||
|
window.removeEventListener('keydown', handleUserInteraction, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agrega event listeners para interacciones del usuario
|
||||||
|
*/
|
||||||
|
function addEventListeners() {
|
||||||
|
debugLog('Agregando event listeners para interacción del usuario');
|
||||||
|
|
||||||
|
// Evento de scroll - cargar en primer scroll
|
||||||
|
window.addEventListener('scroll', handleUserInteraction, { passive: true, once: true });
|
||||||
|
|
||||||
|
// Movimiento de mouse - cargar cuando el usuario mueve el mouse
|
||||||
|
window.addEventListener('mousemove', handleUserInteraction, { passive: true, once: true });
|
||||||
|
|
||||||
|
// Eventos táctiles - cargar en primer toque (móviles)
|
||||||
|
window.addEventListener('touchstart', handleUserInteraction, { passive: true, once: true });
|
||||||
|
|
||||||
|
// Eventos de click - cargar en primer click
|
||||||
|
window.addEventListener('click', handleUserInteraction, { passive: true, once: true });
|
||||||
|
|
||||||
|
// Eventos de teclado - cargar en primera pulsación de tecla
|
||||||
|
window.addEventListener('keydown', handleUserInteraction, { passive: true, once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establece timeout de fallback para cargar AdSense después del tiempo especificado
|
||||||
|
*/
|
||||||
|
function setTimeoutFallback() {
|
||||||
|
debugLog('Estableciendo timeout de fallback (' + CONFIG.timeout + 'ms)');
|
||||||
|
|
||||||
|
loadTimeout = setTimeout(function() {
|
||||||
|
debugLog('Timeout alcanzado, cargando AdSense');
|
||||||
|
loadAdSense();
|
||||||
|
}, CONFIG.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activa slots de AdSense insertados dinamicamente
|
||||||
|
* Escucha el evento 'roi-adsense-activate' disparado por otros scripts
|
||||||
|
*/
|
||||||
|
function setupDynamicAdsListener() {
|
||||||
|
window.addEventListener('roi-adsense-activate', function() {
|
||||||
|
debugLog('Evento roi-adsense-activate recibido');
|
||||||
|
|
||||||
|
// Si AdSense aun no ha cargado, forzar carga ahora
|
||||||
|
if (!adsenseLoaded) {
|
||||||
|
debugLog('AdSense no cargado, forzando carga...');
|
||||||
|
loadAdSense();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdSense ya cargado - activar nuevos slots
|
||||||
|
debugLog('Activando nuevos slots dinamicos...');
|
||||||
|
activateDynamicSlots();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activa slots de AdSense que fueron insertados despues de la carga inicial
|
||||||
|
*/
|
||||||
|
function activateDynamicSlots() {
|
||||||
|
// Buscar scripts de push que aun no han sido ejecutados
|
||||||
|
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
|
||||||
|
|
||||||
|
if (pendingPushScripts.length === 0) {
|
||||||
|
debugLog('No hay slots pendientes por activar');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)');
|
||||||
|
|
||||||
|
// Asegurar que adsbygoogle existe
|
||||||
|
window.adsbygoogle = window.adsbygoogle || [];
|
||||||
|
|
||||||
|
pendingPushScripts.forEach(function(oldScript) {
|
||||||
|
try {
|
||||||
|
// Crear nuevo script ejecutable
|
||||||
|
var newScript = document.createElement('script');
|
||||||
|
newScript.type = 'text/javascript';
|
||||||
|
newScript.innerHTML = oldScript.innerHTML;
|
||||||
|
|
||||||
|
// Reemplazar el placeholder con el script real
|
||||||
|
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||||
|
} catch (e) {
|
||||||
|
debugLog('Error activando slot: ' + e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicializa el cargador retrasado de AdSense
|
||||||
|
*/
|
||||||
|
function init() {
|
||||||
|
// =========================================================================
|
||||||
|
// NUEVO: Siempre configurar listener para ads dinamicos
|
||||||
|
// IMPORTANTE: Esto debe ejecutarse ANTES del early return
|
||||||
|
// porque los ads dinamicos pueden necesitar activarse aunque
|
||||||
|
// el delay global este deshabilitado
|
||||||
|
// =========================================================================
|
||||||
|
setupDynamicAdsListener();
|
||||||
|
debugLog('Listener para ads dinamicos configurado');
|
||||||
|
|
||||||
|
// Verificar si el retardo de AdSense está habilitado
|
||||||
|
if (!window.roiAdsenseDelayed) {
|
||||||
|
debugLog('Retardo de AdSense no habilitado');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('Inicializando cargador retrasado de AdSense');
|
||||||
|
|
||||||
|
// Verificar si la página ya está interactiva o completa
|
||||||
|
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
||||||
|
debugLog('Página ya cargada, iniciando listeners');
|
||||||
|
addEventListeners();
|
||||||
|
setTimeoutFallback();
|
||||||
|
} else {
|
||||||
|
// Esperar a que el DOM esté listo
|
||||||
|
debugLog('Esperando a DOMContentLoaded');
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
debugLog('DOMContentLoaded disparado');
|
||||||
|
addEventListeners();
|
||||||
|
setTimeoutFallback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iniciar inicialización
|
||||||
|
init();
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -489,6 +489,25 @@ function roi_enqueue_adsense_loader() {
|
|||||||
'strategy' => 'defer',
|
'strategy' => 'defer',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Pasar configuración de lazy loading a JavaScript
|
||||||
|
// Nota: wp_add_inline_script funciona mejor con strategy => 'defer' que wp_localize_script
|
||||||
|
$lazy_enabled = roi_get_component_setting('adsense-placement', 'behavior', 'lazy_loading_enabled', true);
|
||||||
|
$lazy_rootmargin = roi_get_component_setting('adsense-placement', 'behavior', 'lazy_rootmargin', '200');
|
||||||
|
$lazy_fill_timeout = roi_get_component_setting('adsense-placement', 'behavior', 'lazy_fill_timeout', '5000');
|
||||||
|
|
||||||
|
$config = array(
|
||||||
|
'lazyEnabled' => filter_var($lazy_enabled, FILTER_VALIDATE_BOOLEAN),
|
||||||
|
'rootMargin' => (int) $lazy_rootmargin . 'px 0px',
|
||||||
|
'fillTimeout' => (int) $lazy_fill_timeout,
|
||||||
|
'debug' => defined('WP_DEBUG') && WP_DEBUG ? true : false,
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_add_inline_script(
|
||||||
|
'roi-adsense-loader',
|
||||||
|
'window.roiAdsenseConfig = ' . wp_json_encode($config) . ';',
|
||||||
|
'before'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
add_action('wp_enqueue_scripts', 'roi_enqueue_adsense_loader', 10);
|
add_action('wp_enqueue_scripts', 'roi_enqueue_adsense_loader', 10);
|
||||||
|
|||||||
@@ -9,13 +9,30 @@ use ROITheme\Public\AdsensePlacement\Infrastructure\Ui\AdsensePlacementRenderer;
|
|||||||
* Inyecta anuncios dentro del contenido del post
|
* Inyecta anuncios dentro del contenido del post
|
||||||
* via filtro the_content
|
* via filtro the_content
|
||||||
*
|
*
|
||||||
* Soporta:
|
* Soporta dos modos:
|
||||||
* - Modo aleatorio (random) con posiciones variables
|
* - Solo parrafos: Logica clasica solo con parrafos (usa config de behavior)
|
||||||
* - Configuracion de 1-8 ads maximo
|
* - Avanzado: Multiples tipos de elementos (H2, H3, p, img, lists, blockquotes, tables)
|
||||||
* - Espacio minimo entre anuncios
|
*
|
||||||
|
* El modo se determina por incontent_mode:
|
||||||
|
* - "paragraphs_only": usa config de behavior (insercion solo en parrafos)
|
||||||
|
* - Otros: usa config de incontent_advanced
|
||||||
*/
|
*/
|
||||||
final class ContentAdInjector
|
final class ContentAdInjector
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Prioridades de elementos para seleccion
|
||||||
|
* Mayor = mas importante
|
||||||
|
*/
|
||||||
|
private const ELEMENT_PRIORITIES = [
|
||||||
|
'h2' => 10,
|
||||||
|
'p' => 8,
|
||||||
|
'h3' => 7,
|
||||||
|
'image' => 6,
|
||||||
|
'list' => 5,
|
||||||
|
'blockquote' => 4,
|
||||||
|
'table' => 3,
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private array $settings,
|
private array $settings,
|
||||||
private AdsensePlacementRenderer $renderer
|
private AdsensePlacementRenderer $renderer
|
||||||
@@ -25,18 +42,37 @@ final class ContentAdInjector
|
|||||||
* Filtra the_content para insertar anuncios
|
* Filtra the_content para insertar anuncios
|
||||||
*/
|
*/
|
||||||
public function inject(string $content): string
|
public function inject(string $content): string
|
||||||
|
{
|
||||||
|
// DEBUG TEMPORAL
|
||||||
|
$debug = '<!-- ROI_AD_DEBUG: inject() called, content length=' . strlen($content) . ' -->';
|
||||||
|
|
||||||
|
// PASO 0: Validar longitud minima (aplica a todos los modos)
|
||||||
|
$minLength = (int)($this->settings['forms']['min_content_length'] ?? 500);
|
||||||
|
if (strlen(strip_tags($content)) < $minLength) {
|
||||||
|
return $debug . '<!-- SKIP: too short -->' . $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determinar modo de operacion
|
||||||
|
$mode = $this->settings['incontent_advanced']['incontent_mode'] ?? 'paragraphs_only';
|
||||||
|
$debug .= '<!-- MODE=' . $mode . ' -->';
|
||||||
|
|
||||||
|
if ($mode === 'paragraphs_only') {
|
||||||
|
return $debug . $this->injectParagraphsOnly($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $debug . $this->injectAdvanced($content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modo solo parrafos: logica clasica que inserta anuncios unicamente despues de parrafos
|
||||||
|
*/
|
||||||
|
private function injectParagraphsOnly(string $content): string
|
||||||
{
|
{
|
||||||
if (!($this->settings['behavior']['post_content_enabled'] ?? false)) {
|
if (!($this->settings['behavior']['post_content_enabled'] ?? false)) {
|
||||||
return $content;
|
return $content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar longitud minima
|
// Obtener configuracion de behavior (modo solo parrafos)
|
||||||
$minLength = (int)($this->settings['forms']['min_content_length'] ?? 500);
|
|
||||||
if (strlen(strip_tags($content)) < $minLength) {
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtener configuracion
|
|
||||||
$minAds = (int)($this->settings['behavior']['post_content_min_ads'] ?? 1);
|
$minAds = (int)($this->settings['behavior']['post_content_min_ads'] ?? 1);
|
||||||
$maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 3);
|
$maxAds = (int)($this->settings['behavior']['post_content_max_ads'] ?? 3);
|
||||||
$afterParagraphs = (int)($this->settings['behavior']['post_content_after_paragraphs'] ?? 3);
|
$afterParagraphs = (int)($this->settings['behavior']['post_content_after_paragraphs'] ?? 3);
|
||||||
@@ -58,7 +94,7 @@ final class ContentAdInjector
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calcular posiciones de insercion
|
// Calcular posiciones de insercion
|
||||||
$adPositions = $this->calculateAdPositions(
|
$adPositions = $this->calculateParagraphsOnlyPositions(
|
||||||
$totalParagraphs,
|
$totalParagraphs,
|
||||||
$afterParagraphs,
|
$afterParagraphs,
|
||||||
$minBetween,
|
$minBetween,
|
||||||
@@ -72,9 +108,452 @@ final class ContentAdInjector
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reconstruir contenido con anuncios insertados
|
// Reconstruir contenido con anuncios insertados
|
||||||
return $this->buildContentWithAds($paragraphs, $adPositions);
|
return $this->buildParagraphsOnlyContent($paragraphs, $adPositions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modo avanzado: multiples tipos de elementos
|
||||||
|
*/
|
||||||
|
private function injectAdvanced(string $content): string
|
||||||
|
{
|
||||||
|
$config = $this->settings['incontent_advanced'] ?? [];
|
||||||
|
$debug = '<!-- ADV: config keys=' . implode(',', array_keys($config)) . ' -->';
|
||||||
|
|
||||||
|
// Obtener configuracion
|
||||||
|
$maxAds = (int)($config['incontent_max_total_ads'] ?? 8);
|
||||||
|
$minSpacing = (int)($config['incontent_min_spacing'] ?? 3);
|
||||||
|
$priorityMode = $config['incontent_priority_mode'] ?? 'position';
|
||||||
|
$format = $config['incontent_format'] ?? 'in-article';
|
||||||
|
$debug .= '<!-- ADV: maxAds=' . $maxAds . ' minSpacing=' . $minSpacing . ' -->';
|
||||||
|
|
||||||
|
// PASO 1: Escanear contenido para encontrar todas las ubicaciones
|
||||||
|
$locations = $this->scanContent($content);
|
||||||
|
$debug .= '<!-- ADV: scanContent found ' . count($locations) . ' locations -->';
|
||||||
|
|
||||||
|
if (empty($locations)) {
|
||||||
|
return $debug . '<!-- ADV: NO locations found -->' . $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASO 2: Filtrar por configuracion (enabled)
|
||||||
|
$locations = $this->filterByEnabled($locations, $config);
|
||||||
|
$debug .= '<!-- ADV: filterByEnabled left ' . count($locations) . ' -->';
|
||||||
|
|
||||||
|
if (empty($locations)) {
|
||||||
|
return $debug . '<!-- ADV: EMPTY after filterByEnabled -->' . $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASO 3: Aplicar probabilidad deterministica
|
||||||
|
$postId = get_the_ID() ?: 0;
|
||||||
|
$locations = $this->applyProbability($locations, $config, $postId);
|
||||||
|
$debug .= '<!-- ADV: applyProbability left ' . count($locations) . ' -->';
|
||||||
|
|
||||||
|
if (empty($locations)) {
|
||||||
|
return $debug . '<!-- ADV: EMPTY after applyProbability -->' . $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASO 4-5: Filtrar por espaciado y limite (segun priority_mode)
|
||||||
|
if ($priorityMode === 'priority') {
|
||||||
|
$locations = $this->filterByPriorityFirst($locations, $minSpacing, $maxAds);
|
||||||
|
} else {
|
||||||
|
$locations = $this->filterByPositionFirst($locations, $minSpacing, $maxAds);
|
||||||
|
}
|
||||||
|
$debug .= '<!-- ADV: filterBySpacing left ' . count($locations) . ' -->';
|
||||||
|
|
||||||
|
if (empty($locations)) {
|
||||||
|
return $debug . '<!-- ADV: EMPTY after filterBySpacing -->' . $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordenar por posicion para insercion correcta
|
||||||
|
usort($locations, fn($a, $b) => $a['position'] <=> $b['position']);
|
||||||
|
|
||||||
|
$debug .= '<!-- ADV: INSERTING ' . count($locations) . ' ads -->';
|
||||||
|
|
||||||
|
// PASO 6: Insertar anuncios
|
||||||
|
return $debug . $this->insertAds($content, $locations, $format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PASO 1: Escanea el contenido para encontrar ubicaciones elegibles
|
||||||
|
*
|
||||||
|
* IMPORTANTE: No inserta anuncios:
|
||||||
|
* - Dentro de tablas (<table>...</table>)
|
||||||
|
* - Dentro de embeds/iframes (YouTube, etc.)
|
||||||
|
* - Despues de tablas (tables excluidas completamente)
|
||||||
|
*
|
||||||
|
* @return array{position: int, type: string, tag: string, element_index: int}[]
|
||||||
|
*/
|
||||||
|
private function scanContent(string $content): array
|
||||||
|
{
|
||||||
|
$locations = [];
|
||||||
|
$elementIndex = 0;
|
||||||
|
|
||||||
|
// Primero, mapear zonas prohibidas (tablas, iframes, embeds)
|
||||||
|
$forbiddenZones = $this->mapForbiddenZones($content);
|
||||||
|
|
||||||
|
// Regex para encontrar tags de cierre de elementos de bloque
|
||||||
|
// NOTA: Excluimos </table> - no queremos insertar despues de tablas
|
||||||
|
$pattern = '/(<\/(?:p|h2|h3|figure|ul|ol|blockquote)>)/i';
|
||||||
|
|
||||||
|
// Encontrar todas las coincidencias con sus posiciones
|
||||||
|
if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
|
||||||
|
foreach ($matches[0] as $match) {
|
||||||
|
$tag = strtolower($match[0]);
|
||||||
|
$position = $match[1] + strlen($match[0]); // Posicion despues del tag
|
||||||
|
|
||||||
|
// Verificar que no este dentro de una zona prohibida
|
||||||
|
if ($this->isInForbiddenZone($position, $forbiddenZones)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $this->getTypeFromTag($tag);
|
||||||
|
if ($type) {
|
||||||
|
$locations[] = [
|
||||||
|
'position' => $position,
|
||||||
|
'type' => $type,
|
||||||
|
'tag' => $tag,
|
||||||
|
'element_index' => $elementIndex++,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detectar imagenes standalone (no dentro de figure ni zonas prohibidas)
|
||||||
|
$locations = array_merge($locations, $this->scanStandaloneImages($content, $elementIndex, $forbiddenZones));
|
||||||
|
|
||||||
|
// Validar listas (minimo 3 items)
|
||||||
|
$locations = $this->validateLists($content, $locations);
|
||||||
|
|
||||||
|
// Ordenar por posicion
|
||||||
|
usort($locations, fn($a, $b) => $a['position'] <=> $b['position']);
|
||||||
|
|
||||||
|
// Reasignar indices de elemento
|
||||||
|
foreach ($locations as $i => &$loc) {
|
||||||
|
$loc['element_index'] = $i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $locations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapea zonas donde NO se deben insertar anuncios
|
||||||
|
* Incluye: tablas, iframes, embeds de video
|
||||||
|
*
|
||||||
|
* @return array{start: int, end: int}[]
|
||||||
|
*/
|
||||||
|
private function mapForbiddenZones(string $content): array
|
||||||
|
{
|
||||||
|
$zones = [];
|
||||||
|
$contentLength = strlen($content);
|
||||||
|
|
||||||
|
// Tablas: buscar cada <table> y su </table> correspondiente
|
||||||
|
$this->findMatchingTags($content, 'table', $zones);
|
||||||
|
|
||||||
|
// Iframes (YouTube, Vimeo, etc)
|
||||||
|
$this->findMatchingTags($content, 'iframe', $zones);
|
||||||
|
|
||||||
|
// Figure con clase wp-block-embed (embeds de WordPress)
|
||||||
|
if (preg_match_all('/<figure[^>]*class="[^"]*wp-block-embed[^"]*"[^>]*>/i', $content, $matches, PREG_OFFSET_CAPTURE)) {
|
||||||
|
foreach ($matches[0] as $match) {
|
||||||
|
$startPos = $match[1];
|
||||||
|
$closePos = strpos($content, '</figure>', $startPos);
|
||||||
|
if ($closePos !== false) {
|
||||||
|
$zones[] = [
|
||||||
|
'start' => $startPos,
|
||||||
|
'end' => $closePos + strlen('</figure>'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $zones;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encuentra tags de apertura y cierre correspondientes
|
||||||
|
*/
|
||||||
|
private function findMatchingTags(string $content, string $tagName, array &$zones): void
|
||||||
|
{
|
||||||
|
$openTag = '<' . $tagName;
|
||||||
|
$closeTag = '</' . $tagName . '>';
|
||||||
|
$offset = 0;
|
||||||
|
|
||||||
|
while (($startPos = stripos($content, $openTag, $offset)) !== false) {
|
||||||
|
// Buscar el cierre correspondiente
|
||||||
|
$closePos = stripos($content, $closeTag, $startPos);
|
||||||
|
if ($closePos !== false) {
|
||||||
|
$zones[] = [
|
||||||
|
'start' => $startPos,
|
||||||
|
'end' => $closePos + strlen($closeTag),
|
||||||
|
];
|
||||||
|
$offset = $closePos + strlen($closeTag);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si una posicion esta dentro de una zona prohibida
|
||||||
|
*/
|
||||||
|
private function isInForbiddenZone(int $position, array $forbiddenZones): bool
|
||||||
|
{
|
||||||
|
foreach ($forbiddenZones as $zone) {
|
||||||
|
if ($position >= $zone['start'] && $position <= $zone['end']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convierte tag de cierre a tipo de elemento
|
||||||
|
* NOTA: </table> excluido - no insertamos ads despues de tablas
|
||||||
|
*/
|
||||||
|
private function getTypeFromTag(string $tag): ?string
|
||||||
|
{
|
||||||
|
return match ($tag) {
|
||||||
|
'</p>' => 'p',
|
||||||
|
'</h2>' => 'h2',
|
||||||
|
'</h3>' => 'h3',
|
||||||
|
'</figure>' => 'image',
|
||||||
|
'</ul>', '</ol>' => 'list',
|
||||||
|
'</blockquote>' => 'blockquote',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detecta imagenes que no estan dentro de figure ni zonas prohibidas
|
||||||
|
*/
|
||||||
|
private function scanStandaloneImages(string $content, int $startIndex, array $forbiddenZones = []): array
|
||||||
|
{
|
||||||
|
$locations = [];
|
||||||
|
|
||||||
|
// Encontrar todas las imagenes con sus posiciones
|
||||||
|
if (!preg_match_all('/<img[^>]*>/i', $content, $matches, PREG_OFFSET_CAPTURE)) {
|
||||||
|
return $locations;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($matches[0] as $match) {
|
||||||
|
$imgTag = $match[0];
|
||||||
|
$imgPosition = $match[1];
|
||||||
|
|
||||||
|
// Verificar que no este en zona prohibida
|
||||||
|
if ($this->isInForbiddenZone($imgPosition, $forbiddenZones)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si hay un <figure> abierto antes de esta imagen
|
||||||
|
$contentBefore = substr($content, 0, $imgPosition);
|
||||||
|
$lastFigureOpen = strrpos($contentBefore, '<figure');
|
||||||
|
$lastFigureClose = strrpos($contentBefore, '</figure>');
|
||||||
|
|
||||||
|
// Si hay figure abierto sin cerrar, esta imagen esta dentro de figure
|
||||||
|
if ($lastFigureOpen !== false && ($lastFigureClose === false || $lastFigureClose < $lastFigureOpen)) {
|
||||||
|
continue; // Ignorar, se contara con </figure>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imagen standalone - calcular posicion despues del tag
|
||||||
|
$endPosition = $imgPosition + strlen($imgTag);
|
||||||
|
|
||||||
|
// Si la imagen esta seguida de </p> o similar, usar esa posicion
|
||||||
|
$contentAfter = substr($content, $endPosition, 20);
|
||||||
|
if (preg_match('/^\s*<\/p>/i', $contentAfter, $closeMatch)) {
|
||||||
|
// La imagen esta dentro de un parrafo, no es standalone
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$locations[] = [
|
||||||
|
'position' => $endPosition,
|
||||||
|
'type' => 'image',
|
||||||
|
'tag' => $imgTag,
|
||||||
|
'element_index' => $startIndex++,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $locations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida que las listas tengan minimo 3 items
|
||||||
|
*/
|
||||||
|
private function validateLists(string $content, array $locations): array
|
||||||
|
{
|
||||||
|
return array_filter($locations, function ($loc) use ($content) {
|
||||||
|
if ($loc['type'] !== 'list') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encontrar el contenido de la lista
|
||||||
|
$endPos = $loc['position'];
|
||||||
|
$tag = $loc['tag'];
|
||||||
|
$openTag = str_replace('/', '', $tag); // </ul> -> <ul>
|
||||||
|
|
||||||
|
// Buscar hacia atras el tag de apertura
|
||||||
|
$contentBefore = substr($content, 0, $endPos);
|
||||||
|
$lastOpen = strrpos($contentBefore, '<' . substr($openTag, 1)); // <ul o <ol
|
||||||
|
|
||||||
|
if ($lastOpen === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$listContent = substr($content, $lastOpen, $endPos - $lastOpen);
|
||||||
|
|
||||||
|
// Contar items (usando substr_count como indica el spec)
|
||||||
|
$itemCount = substr_count(strtolower($listContent), '<li');
|
||||||
|
|
||||||
|
return $itemCount >= 3;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PASO 2: Filtra ubicaciones por campos enabled
|
||||||
|
*/
|
||||||
|
private function filterByEnabled(array $locations, array $config): array
|
||||||
|
{
|
||||||
|
$enabledTypes = [];
|
||||||
|
|
||||||
|
$typeMapping = [
|
||||||
|
'h2' => 'incontent_after_h2_enabled',
|
||||||
|
'h3' => 'incontent_after_h3_enabled',
|
||||||
|
'p' => 'incontent_after_paragraphs_enabled',
|
||||||
|
'image' => 'incontent_after_images_enabled',
|
||||||
|
'list' => 'incontent_after_lists_enabled',
|
||||||
|
'blockquote' => 'incontent_after_blockquotes_enabled',
|
||||||
|
'table' => 'incontent_after_tables_enabled',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($typeMapping as $type => $field) {
|
||||||
|
if ($config[$field] ?? false) {
|
||||||
|
$enabledTypes[] = $type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter($locations, fn($loc) => in_array($loc['type'], $enabledTypes, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PASO 3: Aplica probabilidad deterministica usando seed del dia
|
||||||
|
*/
|
||||||
|
private function applyProbability(array $locations, array $config, int $postId): array
|
||||||
|
{
|
||||||
|
// Calcular seed deterministico
|
||||||
|
$seed = crc32($postId . date('Y-m-d'));
|
||||||
|
mt_srand($seed);
|
||||||
|
|
||||||
|
$probMapping = [
|
||||||
|
'h2' => 'incontent_after_h2_probability',
|
||||||
|
'h3' => 'incontent_after_h3_probability',
|
||||||
|
'p' => 'incontent_after_paragraphs_probability',
|
||||||
|
'image' => 'incontent_after_images_probability',
|
||||||
|
'list' => 'incontent_after_lists_probability',
|
||||||
|
'blockquote' => 'incontent_after_blockquotes_probability',
|
||||||
|
'table' => 'incontent_after_tables_probability',
|
||||||
|
];
|
||||||
|
|
||||||
|
return array_filter($locations, function ($loc) use ($config, $probMapping) {
|
||||||
|
$field = $probMapping[$loc['type']] ?? null;
|
||||||
|
if (!$field) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$probability = (int)($config[$field] ?? 100);
|
||||||
|
$roll = mt_rand(1, 100);
|
||||||
|
|
||||||
|
return $roll <= $probability;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PASO 4-5 (modo position): Filtrar por espaciado primero, luego por prioridad
|
||||||
|
*/
|
||||||
|
private function filterByPositionFirst(array $locations, int $minSpacing, int $maxAds): array
|
||||||
|
{
|
||||||
|
// PASO 4: Ordenar por posicion y filtrar por espaciado
|
||||||
|
usort($locations, fn($a, $b) => $a['element_index'] <=> $b['element_index']);
|
||||||
|
|
||||||
|
$filtered = [];
|
||||||
|
$lastIndex = -999;
|
||||||
|
|
||||||
|
foreach ($locations as $loc) {
|
||||||
|
if ($loc['element_index'] - $lastIndex >= $minSpacing) {
|
||||||
|
$filtered[] = $loc;
|
||||||
|
$lastIndex = $loc['element_index'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASO 5: Si excede max, seleccionar por prioridad
|
||||||
|
if (count($filtered) > $maxAds) {
|
||||||
|
usort($filtered, fn($a, $b) =>
|
||||||
|
(self::ELEMENT_PRIORITIES[$b['type']] ?? 0) <=> (self::ELEMENT_PRIORITIES[$a['type']] ?? 0)
|
||||||
|
);
|
||||||
|
$filtered = array_slice($filtered, 0, $maxAds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PASO 4-5 (modo priority): Ordenar por prioridad primero, luego aplicar espaciado
|
||||||
|
*/
|
||||||
|
private function filterByPriorityFirst(array $locations, int $minSpacing, int $maxAds): array
|
||||||
|
{
|
||||||
|
// PASO 4: Ordenar por prioridad
|
||||||
|
usort($locations, fn($a, $b) =>
|
||||||
|
(self::ELEMENT_PRIORITIES[$b['type']] ?? 0) <=> (self::ELEMENT_PRIORITIES[$a['type']] ?? 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// PASO 5: Filtrar por espaciado en orden de prioridad
|
||||||
|
$selected = [];
|
||||||
|
$usedIndices = [];
|
||||||
|
|
||||||
|
foreach ($locations as $loc) {
|
||||||
|
if (count($selected) >= $maxAds) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar espaciado con ubicaciones ya seleccionadas
|
||||||
|
$violatesSpacing = false;
|
||||||
|
foreach ($usedIndices as $usedIndex) {
|
||||||
|
if (abs($loc['element_index'] - $usedIndex) < $minSpacing) {
|
||||||
|
$violatesSpacing = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$violatesSpacing) {
|
||||||
|
$selected[] = $loc;
|
||||||
|
$usedIndices[] = $loc['element_index'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PASO 6: Inserta los anuncios en las posiciones calculadas
|
||||||
|
*/
|
||||||
|
private function insertAds(string $content, array $locations, string $format): string
|
||||||
|
{
|
||||||
|
// Insertar de atras hacia adelante para no afectar posiciones
|
||||||
|
$locations = array_reverse($locations);
|
||||||
|
|
||||||
|
$adCount = count($locations);
|
||||||
|
$currentAd = $adCount;
|
||||||
|
|
||||||
|
foreach ($locations as $loc) {
|
||||||
|
$adHtml = $this->renderer->renderSlot($this->settings, 'post-content-adv-' . $currentAd);
|
||||||
|
$content = substr_replace($content, $adHtml, $loc['position'], 0);
|
||||||
|
$currentAd--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// METODOS LEGACY (sin cambios para backward compatibility)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Divide el contenido en parrafos preservando el HTML
|
* Divide el contenido en parrafos preservando el HTML
|
||||||
*/
|
*/
|
||||||
@@ -103,11 +582,11 @@ final class ContentAdInjector
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcula las posiciones donde insertar anuncios
|
* Calcula las posiciones donde insertar anuncios (modo solo parrafos)
|
||||||
*
|
*
|
||||||
* @return int[] Indices de parrafos despues de los cuales insertar ads
|
* @return int[] Indices de parrafos despues de los cuales insertar ads
|
||||||
*/
|
*/
|
||||||
private function calculateAdPositions(
|
private function calculateParagraphsOnlyPositions(
|
||||||
int $totalParagraphs,
|
int $totalParagraphs,
|
||||||
int $afterFirst,
|
int $afterFirst,
|
||||||
int $minBetween,
|
int $minBetween,
|
||||||
@@ -117,7 +596,6 @@ final class ContentAdInjector
|
|||||||
): array {
|
): array {
|
||||||
// Calcular posiciones disponibles respetando el espacio minimo
|
// Calcular posiciones disponibles respetando el espacio minimo
|
||||||
$availablePositions = [];
|
$availablePositions = [];
|
||||||
$lastPosition = $afterFirst; // Primera posicion fija
|
|
||||||
|
|
||||||
// La primera posicion siempre es despues del parrafo indicado
|
// La primera posicion siempre es despues del parrafo indicado
|
||||||
if ($afterFirst < $totalParagraphs) {
|
if ($afterFirst < $totalParagraphs) {
|
||||||
@@ -178,9 +656,9 @@ final class ContentAdInjector
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reconstruye el contenido insertando anuncios en las posiciones indicadas
|
* Reconstruye el contenido insertando anuncios en las posiciones indicadas (modo solo parrafos)
|
||||||
*/
|
*/
|
||||||
private function buildContentWithAds(array $paragraphs, array $adPositions): string
|
private function buildParagraphsOnlyContent(array $paragraphs, array $adPositions): string
|
||||||
{
|
{
|
||||||
$newContent = '';
|
$newContent = '';
|
||||||
$adsInserted = 0;
|
$adsInserted = 0;
|
||||||
|
|||||||
@@ -64,18 +64,70 @@ final class AdsensePlacementRenderer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Generar CSS (usando CSSGeneratorService)
|
// 4. Generar CSS (usando CSSGeneratorService)
|
||||||
|
$lazyEnabled = ($settings['behavior']['lazy_loading_enabled'] ?? true) === true;
|
||||||
|
|
||||||
|
// Estrategia para evitar "flash" de slots vacíos:
|
||||||
|
//
|
||||||
|
// PROBLEMA: Si mostramos slots con min-height, hay un "flash" visible
|
||||||
|
// entre que el slot aparece y AdSense lo marca como unfilled.
|
||||||
|
//
|
||||||
|
// SOLUCIÓN: Ocultar slots INICIALMENTE (height:0, overflow:hidden)
|
||||||
|
// y SOLO mostrarlos cuando tienen data-ad-status="filled".
|
||||||
|
//
|
||||||
|
// NOTA: Usamos height:0 + overflow:hidden en vez de display:none
|
||||||
|
// porque AdSense necesita que el elemento exista en el layout para procesarlo.
|
||||||
|
|
||||||
|
// CSS base: slots COLAPSADOS por defecto (sin flash)
|
||||||
$css = $this->cssGenerator->generate(
|
$css = $this->cssGenerator->generate(
|
||||||
".roi-ad-slot",
|
".roi-ad-slot",
|
||||||
[
|
[
|
||||||
'display' => 'block',
|
|
||||||
'width' => '100%',
|
'width' => '100%',
|
||||||
'min_width' => '300px',
|
'min_width' => '300px',
|
||||||
'margin_top' => '1.5rem',
|
|
||||||
'margin_bottom' => '1.5rem',
|
|
||||||
'text_align' => 'center',
|
'text_align' => 'center',
|
||||||
|
'overflow' => 'hidden',
|
||||||
|
// COLAPSADO por defecto - evita el flash
|
||||||
|
'height' => '0',
|
||||||
|
'margin' => '0',
|
||||||
|
'padding' => '0',
|
||||||
|
'opacity' => '0',
|
||||||
|
'transition' => 'height 0.3s ease, margin 0.3s ease, opacity 0.3s ease',
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// SOLO mostrar cuando AdSense confirma que hay anuncio (filled)
|
||||||
|
// Esto es la clave: el slot permanece oculto hasta confirmación
|
||||||
|
$css .= $this->cssGenerator->generate(
|
||||||
|
".roi-ad-slot:has(ins.adsbygoogle[data-ad-status='filled'])",
|
||||||
|
[
|
||||||
|
'height' => 'auto',
|
||||||
|
'margin_top' => '1.5rem',
|
||||||
|
'margin_bottom' => '1.5rem',
|
||||||
|
'opacity' => '1',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fallback JS: clase añadida por adsense-loader.js cuando detecta filled
|
||||||
|
$css .= $this->cssGenerator->generate('.roi-ad-slot.roi-ad-filled', [
|
||||||
|
'height' => 'auto',
|
||||||
|
'margin_top' => '1.5rem',
|
||||||
|
'margin_bottom' => '1.5rem',
|
||||||
|
'opacity' => '1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Slots unfilled permanecen colapsados (ya lo están por defecto)
|
||||||
|
// Pero añadimos el selector explícito para claridad
|
||||||
|
$css .= $this->cssGenerator->generate(
|
||||||
|
"ins.adsbygoogle[data-ad-status='unfilled']",
|
||||||
|
[
|
||||||
|
'display' => 'none !important',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fallback para navegadores sin soporte :has() - clase JS
|
||||||
|
$css .= $this->cssGenerator->generate('.roi-ad-slot.roi-ad-empty', [
|
||||||
|
'display' => 'none',
|
||||||
|
]);
|
||||||
|
|
||||||
// 5. Generar HTML del anuncio
|
// 5. Generar HTML del anuncio
|
||||||
$html = $this->buildAdHTML(
|
$html = $this->buildAdHTML(
|
||||||
$settings,
|
$settings,
|
||||||
@@ -111,15 +163,24 @@ final class AdsensePlacementRenderer
|
|||||||
{
|
{
|
||||||
$locationKey = str_replace('-', '_', $location);
|
$locationKey = str_replace('-', '_', $location);
|
||||||
|
|
||||||
// Manejar ubicaciones de in-content (post_content_1, post_content_2, etc.)
|
// Manejar ubicaciones de in-content legacy (post_content_1, post_content_2, etc.)
|
||||||
if (preg_match('/^post_content_(\d+)$/', $locationKey, $matches)) {
|
if (preg_match('/^post_content_(\d+)$/', $locationKey, $matches)) {
|
||||||
// In-content ads heredan la configuracion de post_content
|
// In-content ads heredan la configuracion de post_content (modo solo parrafos)
|
||||||
return [
|
return [
|
||||||
'enabled' => $settings['behavior']['post_content_enabled'] ?? false,
|
'enabled' => $settings['behavior']['post_content_enabled'] ?? false,
|
||||||
'format' => $settings['behavior']['post_content_format'] ?? 'in-article',
|
'format' => $settings['behavior']['post_content_format'] ?? 'in-article',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manejar ubicaciones de in-content avanzado (post_content_adv_1, post_content_adv_2, etc.)
|
||||||
|
if (preg_match('/^post_content_adv_(\d+)$/', $locationKey, $matches)) {
|
||||||
|
// In-content ads avanzados usan configuracion de incontent_advanced
|
||||||
|
return [
|
||||||
|
'enabled' => true, // Siempre enabled porque ya pasaron los filtros en ContentAdInjector
|
||||||
|
'format' => $settings['incontent_advanced']['incontent_format'] ?? 'in-article',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Mapeo de ubicaciones a grupos y campos
|
// Mapeo de ubicaciones a grupos y campos
|
||||||
$locationMap = [
|
$locationMap = [
|
||||||
'post_top' => ['group' => 'behavior', 'enabled' => 'post_top_enabled', 'format' => 'post_top_format'],
|
'post_top' => ['group' => 'behavior', 'enabled' => 'post_top_enabled', 'format' => 'post_top_format'],
|
||||||
@@ -152,6 +213,7 @@ final class AdsensePlacementRenderer
|
|||||||
{
|
{
|
||||||
$publisherId = esc_attr($settings['content']['publisher_id'] ?? '');
|
$publisherId = esc_attr($settings['content']['publisher_id'] ?? '');
|
||||||
$delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true;
|
$delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true;
|
||||||
|
$lazyEnabled = ($settings['behavior']['lazy_loading_enabled'] ?? true) === true;
|
||||||
|
|
||||||
if (empty($publisherId)) {
|
if (empty($publisherId)) {
|
||||||
return '';
|
return '';
|
||||||
@@ -165,9 +227,10 @@ final class AdsensePlacementRenderer
|
|||||||
|
|
||||||
$scriptType = $delayEnabled ? 'text/plain' : 'text/javascript';
|
$scriptType = $delayEnabled ? 'text/plain' : 'text/javascript';
|
||||||
$dataAttr = $delayEnabled ? ' data-adsense-push' : '';
|
$dataAttr = $delayEnabled ? ' data-adsense-push' : '';
|
||||||
|
$lazyAttr = $lazyEnabled ? ' data-ad-lazy="true"' : '';
|
||||||
$locationClass = 'roi-ad-' . esc_attr(str_replace('_', '-', $location));
|
$locationClass = 'roi-ad-' . esc_attr(str_replace('_', '-', $location));
|
||||||
|
|
||||||
return $this->generateAdMarkup($format, $publisherId, $slotId, $locationClass, $visClasses, $scriptType, $dataAttr);
|
return $this->generateAdMarkup($format, $publisherId, $slotId, $locationClass, $visClasses, $scriptType, $dataAttr, $lazyAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,68 +271,69 @@ final class AdsensePlacementRenderer
|
|||||||
string $locationClass,
|
string $locationClass,
|
||||||
string $visClasses,
|
string $visClasses,
|
||||||
string $scriptType,
|
string $scriptType,
|
||||||
string $dataAttr
|
string $dataAttr,
|
||||||
|
string $lazyAttr = ''
|
||||||
): string {
|
): string {
|
||||||
$allClasses = trim("{$locationClass} {$visClasses}");
|
$allClasses = trim("{$locationClass} {$visClasses}");
|
||||||
|
|
||||||
return match($format) {
|
return match($format) {
|
||||||
'display' => $this->adDisplay($client, $slot, 728, 90, $allClasses, $scriptType, $dataAttr),
|
'display' => $this->adDisplay($client, $slot, 728, 90, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||||
'display-large' => $this->adDisplay($client, $slot, 970, 250, $allClasses, $scriptType, $dataAttr),
|
'display-large' => $this->adDisplay($client, $slot, 970, 250, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||||
'display-square' => $this->adDisplay($client, $slot, 300, 250, $allClasses, $scriptType, $dataAttr),
|
'display-square' => $this->adDisplay($client, $slot, 300, 250, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||||
'in-article' => $this->adInArticle($client, $slot, $allClasses, $scriptType, $dataAttr),
|
'in-article' => $this->adInArticle($client, $slot, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||||
'autorelaxed' => $this->adAutorelaxed($client, $slot, $allClasses, $scriptType, $dataAttr),
|
'autorelaxed' => $this->adAutorelaxed($client, $slot, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||||
default => $this->adAuto($client, $slot, $allClasses, $scriptType, $dataAttr),
|
default => $this->adAuto($client, $slot, $allClasses, $scriptType, $dataAttr, $lazyAttr),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private function adDisplay(string $c, string $s, int $w, int $h, string $cl, string $t, string $a): string
|
private function adDisplay(string $c, string $s, int $w, int $h, string $cl, string $t, string $a, string $lazy = ''): string
|
||||||
{
|
{
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<div class="roi-ad-slot %s">
|
'<div class="roi-ad-slot %s"%s>
|
||||||
<ins class="adsbygoogle" style="display:inline-block;width:%dpx;height:%dpx"
|
<ins class="adsbygoogle" style="display:inline-block;width:%dpx;height:%dpx"
|
||||||
data-ad-client="%s" data-ad-slot="%s"></ins>
|
data-ad-client="%s" data-ad-slot="%s"></ins>
|
||||||
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
||||||
</div>',
|
</div>',
|
||||||
esc_attr($cl), $w, $h, esc_attr($c), esc_attr($s), $t, $a
|
esc_attr($cl), $lazy, $w, $h, esc_attr($c), esc_attr($s), $t, $a
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function adAuto(string $c, string $s, string $cl, string $t, string $a): string
|
private function adAuto(string $c, string $s, string $cl, string $t, string $a, string $lazy = ''): string
|
||||||
{
|
{
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<div class="roi-ad-slot %s">
|
'<div class="roi-ad-slot %s"%s>
|
||||||
<ins class="adsbygoogle" style="display:block;min-height:250px"
|
<ins class="adsbygoogle" style="display:block;min-height:250px"
|
||||||
data-ad-client="%s" data-ad-slot="%s"
|
data-ad-client="%s" data-ad-slot="%s"
|
||||||
data-ad-format="auto" data-full-width-responsive="true"></ins>
|
data-ad-format="auto" data-full-width-responsive="true"></ins>
|
||||||
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
||||||
</div>',
|
</div>',
|
||||||
esc_attr($cl), esc_attr($c), esc_attr($s), $t, $a
|
esc_attr($cl), $lazy, esc_attr($c), esc_attr($s), $t, $a
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function adInArticle(string $c, string $s, string $cl, string $t, string $a): string
|
private function adInArticle(string $c, string $s, string $cl, string $t, string $a, string $lazy = ''): string
|
||||||
{
|
{
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<div class="roi-ad-slot %s">
|
'<div class="roi-ad-slot %s"%s>
|
||||||
<ins class="adsbygoogle" style="display:block;text-align:center;min-height:200px"
|
<ins class="adsbygoogle" style="display:block;text-align:center;min-height:200px"
|
||||||
data-ad-layout="in-article" data-ad-format="fluid"
|
data-ad-layout="in-article" data-ad-format="fluid"
|
||||||
data-ad-client="%s" data-ad-slot="%s"></ins>
|
data-ad-client="%s" data-ad-slot="%s"></ins>
|
||||||
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
||||||
</div>',
|
</div>',
|
||||||
esc_attr($cl), esc_attr($c), esc_attr($s), $t, $a
|
esc_attr($cl), $lazy, esc_attr($c), esc_attr($s), $t, $a
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function adAutorelaxed(string $c, string $s, string $cl, string $t, string $a): string
|
private function adAutorelaxed(string $c, string $s, string $cl, string $t, string $a, string $lazy = ''): string
|
||||||
{
|
{
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<div class="roi-ad-slot %s">
|
'<div class="roi-ad-slot %s"%s>
|
||||||
<ins class="adsbygoogle" style="display:block;min-height:280px"
|
<ins class="adsbygoogle" style="display:block;min-height:280px"
|
||||||
data-ad-format="autorelaxed"
|
data-ad-format="autorelaxed"
|
||||||
data-ad-client="%s" data-ad-slot="%s"></ins>
|
data-ad-client="%s" data-ad-slot="%s"></ins>
|
||||||
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
<script type="%s"%s>(adsbygoogle = window.adsbygoogle || []).push({});</script>
|
||||||
</div>',
|
</div>',
|
||||||
esc_attr($cl), esc_attr($c), esc_attr($s), $t, $a
|
esc_attr($cl), $lazy, esc_attr($c), esc_attr($s), $t, $a
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"component_name": "adsense-placement",
|
"component_name": "adsense-placement",
|
||||||
"version": "1.3.0",
|
"version": "1.5.0",
|
||||||
"description": "Control de AdSense y Google Analytics - Con Anchor y Vignette Ads",
|
"description": "Control de AdSense y Google Analytics - Con In-Content Ads Avanzado",
|
||||||
"groups": {
|
"groups": {
|
||||||
"visibility": {
|
"visibility": {
|
||||||
"label": "Activacion",
|
"label": "Activacion",
|
||||||
@@ -113,6 +113,171 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"incontent_advanced": {
|
||||||
|
"label": "In-Content Ads Avanzado",
|
||||||
|
"priority": 69,
|
||||||
|
"fields": {
|
||||||
|
"incontent_mode": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Estrategia de insercion",
|
||||||
|
"default": "paragraphs_only",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"paragraphs_only": "Solo parrafos (clasico)",
|
||||||
|
"conservative": "Conservador - H2 y parrafos",
|
||||||
|
"balanced": "Balanceado - Multiples elementos",
|
||||||
|
"aggressive": "Intensivo - Todos los elementos",
|
||||||
|
"custom": "Personalizado"
|
||||||
|
},
|
||||||
|
"description": "Define donde se insertaran los anuncios dentro del contenido del post."
|
||||||
|
},
|
||||||
|
"incontent_after_h2_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de H2",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de encabezados H2"
|
||||||
|
},
|
||||||
|
"incontent_after_h2_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad H2",
|
||||||
|
"default": "100",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"],
|
||||||
|
"description": "Porcentaje de probabilidad de insercion"
|
||||||
|
},
|
||||||
|
"incontent_after_h3_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de H3",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de encabezados H3"
|
||||||
|
},
|
||||||
|
"incontent_after_h3_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad H3",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_paragraphs_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de parrafos",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de parrafos (ubicacion tradicional)"
|
||||||
|
},
|
||||||
|
"incontent_after_paragraphs_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad parrafos",
|
||||||
|
"default": "75",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"],
|
||||||
|
"description": "Porcentaje de probabilidad de insercion despues de parrafos"
|
||||||
|
},
|
||||||
|
"incontent_after_images_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de imagenes",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de figure o img standalone"
|
||||||
|
},
|
||||||
|
"incontent_after_images_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad imagenes",
|
||||||
|
"default": "75",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_lists_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de listas",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de ul/ol (minimo 3 items)"
|
||||||
|
},
|
||||||
|
"incontent_after_lists_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad listas",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_blockquotes_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de blockquotes",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de citas en bloque"
|
||||||
|
},
|
||||||
|
"incontent_after_blockquotes_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad blockquotes",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_tables_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de tablas",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de tablas"
|
||||||
|
},
|
||||||
|
"incontent_after_tables_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad tablas",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_max_total_ads": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Maximo total de ads",
|
||||||
|
"default": "8",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25"],
|
||||||
|
"description": "Cantidad maxima de anuncios in-content por post"
|
||||||
|
},
|
||||||
|
"incontent_min_spacing": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Espaciado minimo",
|
||||||
|
"default": "3",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"1": "1 elemento",
|
||||||
|
"2": "2 elementos",
|
||||||
|
"3": "3 elementos",
|
||||||
|
"4": "4 elementos",
|
||||||
|
"5": "5 elementos",
|
||||||
|
"6": "6 elementos"
|
||||||
|
},
|
||||||
|
"description": "Minimo de elementos de bloque entre anuncios"
|
||||||
|
},
|
||||||
|
"incontent_format": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Formato de ads",
|
||||||
|
"default": "in-article",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"in-article": "In-Article (fluid)",
|
||||||
|
"auto": "Auto (responsive)"
|
||||||
|
},
|
||||||
|
"description": "Formato de anuncio para todas las ubicaciones in-content"
|
||||||
|
},
|
||||||
|
"incontent_priority_mode": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Estrategia de seleccion",
|
||||||
|
"default": "position",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"position": "Por posicion (distribucion uniforme)",
|
||||||
|
"priority": "Por prioridad (maximizar H2/H3)"
|
||||||
|
},
|
||||||
|
"description": "Como resolver conflictos cuando dos ubicaciones estan muy cerca"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"label": "Ubicaciones en Posts",
|
"label": "Ubicaciones en Posts",
|
||||||
"priority": 70,
|
"priority": 70,
|
||||||
@@ -258,6 +423,41 @@
|
|||||||
"700": "700px (Debajo del fold)"
|
"700": "700px (Debajo del fold)"
|
||||||
},
|
},
|
||||||
"description": "Distancia vertical desde el top del viewport"
|
"description": "Distancia vertical desde el top del viewport"
|
||||||
|
},
|
||||||
|
"lazy_loading_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Lazy Loading de Anuncios",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Cargar anuncios individualmente al entrar al viewport (mejora fill rate)"
|
||||||
|
},
|
||||||
|
"lazy_rootmargin": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Pre-carga (px antes del viewport)",
|
||||||
|
"default": "200",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"0": "0px (sin pre-carga)",
|
||||||
|
"100": "100px",
|
||||||
|
"200": "200px (recomendado)",
|
||||||
|
"300": "300px",
|
||||||
|
"400": "400px",
|
||||||
|
"500": "500px"
|
||||||
|
},
|
||||||
|
"description": "Pixeles de anticipacion para iniciar carga de anuncio"
|
||||||
|
},
|
||||||
|
"lazy_fill_timeout": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Timeout de llenado (ms)",
|
||||||
|
"default": "5000",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"3000": "3 segundos",
|
||||||
|
"5000": "5 segundos (recomendado)",
|
||||||
|
"7000": "7 segundos",
|
||||||
|
"10000": "10 segundos"
|
||||||
|
},
|
||||||
|
"description": "Tiempo maximo para esperar contenido de Google antes de ocultar slot"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
82
Shared/Infrastructure/Hooks/CacheFirstHooksRegistrar.php
Normal file
82
Shared/Infrastructure/Hooks/CacheFirstHooksRegistrar.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Hooks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra hooks para arquitectura cache-first.
|
||||||
|
*
|
||||||
|
* Permite que plugins externos evalúen condiciones ANTES de servir páginas,
|
||||||
|
* sin bloquear el cache de WordPress.
|
||||||
|
*
|
||||||
|
* @see openspec/specs/cache-first-architecture/spec.md
|
||||||
|
* @package ROITheme\Shared\Infrastructure\Hooks
|
||||||
|
*/
|
||||||
|
final class CacheFirstHooksRegistrar
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Registra los hooks de cache-first.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
add_action('template_redirect', [$this, 'fireBeforePageServe'], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispara hook para que plugins externos evalúen acceso.
|
||||||
|
*
|
||||||
|
* Solo se dispara para:
|
||||||
|
* - Páginas singulares (posts, pages, CPTs)
|
||||||
|
* - Visitantes NO logueados (cache no aplica a usuarios logueados)
|
||||||
|
*
|
||||||
|
* Los plugins pueden llamar wp_safe_redirect() + exit para bloquear.
|
||||||
|
* Si no hacen nada, la página se sirve normalmente (con cache si disponible).
|
||||||
|
*/
|
||||||
|
public function fireBeforePageServe(): void
|
||||||
|
{
|
||||||
|
// No para usuarios logueados (cache no aplica, no tiene sentido evaluar)
|
||||||
|
if (is_user_logged_in()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo páginas singulares
|
||||||
|
if (!is_singular()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No en admin/ajax/cron/REST
|
||||||
|
if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defined('REST_REQUEST') && REST_REQUEST) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$post_id = get_queried_object_id();
|
||||||
|
|
||||||
|
if ($post_id > 0) {
|
||||||
|
/**
|
||||||
|
* Hook: roi_theme_before_page_serve
|
||||||
|
*
|
||||||
|
* Permite que plugins externos evalúen condiciones antes de servir página.
|
||||||
|
*
|
||||||
|
* Uso típico:
|
||||||
|
* - Rate limiters (límite de vistas por IP)
|
||||||
|
* - Membership plugins (verificar acceso)
|
||||||
|
* - Geolocation restrictions
|
||||||
|
*
|
||||||
|
* Para bloquear acceso:
|
||||||
|
* wp_safe_redirect('/pagina-destino/', 302);
|
||||||
|
* exit;
|
||||||
|
*
|
||||||
|
* Para permitir acceso:
|
||||||
|
* return; // La página se servirá (con cache si disponible)
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @param int $post_id ID del post/page que se va a servir
|
||||||
|
*/
|
||||||
|
do_action('roi_theme_before_page_serve', $post_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ROI THEME DEBUG MODE
|
||||||
|
// =============================================================================
|
||||||
|
// Para activar el modo debug, agregar en wp-config.php:
|
||||||
|
// define('ROI_DEBUG', true);
|
||||||
|
//
|
||||||
|
// IMPORTANTE: Mantener desactivado en producción para evitar logs de GB.
|
||||||
|
// =============================================================================
|
||||||
|
if (!defined('ROI_DEBUG')) {
|
||||||
|
define('ROI_DEBUG', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log de debug condicional para ROI Theme.
|
||||||
|
* Solo escribe al log si ROI_DEBUG está activado.
|
||||||
|
*/
|
||||||
|
function roi_debug_log(string $message): void {
|
||||||
|
if (ROI_DEBUG) {
|
||||||
|
error_log($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// AUTOLOADER PARA COMPONENTES
|
// AUTOLOADER PARA COMPONENTES
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -181,7 +203,7 @@ function roi_render_component(string $componentName): string {
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// DEBUG: Trace component rendering
|
// DEBUG: Trace component rendering
|
||||||
error_log("ROI Theme DEBUG: roi_render_component called with: {$componentName}");
|
roi_debug_log("ROI Theme DEBUG: roi_render_component called with: {$componentName}");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Obtener datos del componente desde BD normalizada
|
// Obtener datos del componente desde BD normalizada
|
||||||
@@ -297,9 +319,9 @@ function roi_render_component(string $componentName): string {
|
|||||||
$renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator);
|
$renderer = new \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer($cssGenerator);
|
||||||
break;
|
break;
|
||||||
case 'hero':
|
case 'hero':
|
||||||
error_log("ROI Theme DEBUG: Creating HeroRenderer");
|
roi_debug_log("ROI Theme DEBUG: Creating HeroRenderer");
|
||||||
$renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator);
|
$renderer = new \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer($cssGenerator);
|
||||||
error_log("ROI Theme DEBUG: HeroRenderer created successfully");
|
roi_debug_log("ROI Theme DEBUG: HeroRenderer created successfully");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Componentes sin soporte de CSS Crítico (below-the-fold)
|
// Componentes sin soporte de CSS Crítico (below-the-fold)
|
||||||
@@ -339,13 +361,13 @@ function roi_render_component(string $componentName): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$renderer) {
|
if (!$renderer) {
|
||||||
error_log("ROI Theme DEBUG: No renderer for {$componentName}");
|
roi_debug_log("ROI Theme DEBUG: No renderer for {$componentName}");
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log("ROI Theme DEBUG: Calling render() for {$componentName}");
|
roi_debug_log("ROI Theme DEBUG: Calling render() for {$componentName}");
|
||||||
$output = $renderer->render($component);
|
$output = $renderer->render($component);
|
||||||
error_log("ROI Theme DEBUG: render() returned " . strlen($output) . " chars for {$componentName}");
|
roi_debug_log("ROI Theme DEBUG: render() returned " . strlen($output) . " chars for {$componentName}");
|
||||||
return $output;
|
return $output;
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Definir constante de versión del tema
|
// Definir constante de versión del tema
|
||||||
define('ROI_VERSION', '1.0.20');
|
define('ROI_VERSION', '1.0.28');
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 1. CARGAR AUTOLOADER MANUAL
|
// 1. CARGAR AUTOLOADER MANUAL
|
||||||
@@ -174,6 +174,12 @@ try {
|
|||||||
);
|
);
|
||||||
$youtubeFacadeHooksRegistrar->register();
|
$youtubeFacadeHooksRegistrar->register();
|
||||||
|
|
||||||
|
// === CACHE-FIRST ARCHITECTURE (Plan 1000.01) ===
|
||||||
|
// Hook para plugins externos que necesitan evaluar acceso antes de servir página
|
||||||
|
// @see openspec/specs/cache-first-architecture/spec.md
|
||||||
|
$cacheFirstHooksRegistrar = new \ROITheme\Shared\Infrastructure\Hooks\CacheFirstHooksRegistrar();
|
||||||
|
$cacheFirstHooksRegistrar->register();
|
||||||
|
|
||||||
// Log en modo debug
|
// Log en modo debug
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
error_log('ROI Theme: Admin Panel initialized successfully');
|
error_log('ROI Theme: Admin Panel initialized successfully');
|
||||||
|
|||||||
471
openspec/changes/add-advanced-incontent-ads/design.md
Normal file
471
openspec/changes/add-advanced-incontent-ads/design.md
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
# Design: Sistema Avanzado de In-Content Ads
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
El sitio analisisdepreciosunitarios.com ha experimentado una reduccion del 50% en ingresos de AdSense. El analisis indica que el sistema actual solo inserta anuncios despues de parrafos, desperdiciando oportunidades de insercion despues de otros elementos estructurales del contenido (encabezados, imagenes, listas, etc.).
|
||||||
|
|
||||||
|
### Stakeholders
|
||||||
|
- Propietario del sitio (monetizacion)
|
||||||
|
- Usuarios (experiencia de lectura)
|
||||||
|
- Google AdSense (politicas de densidad)
|
||||||
|
|
||||||
|
### Constraints
|
||||||
|
- Politicas de AdSense: No mas de 3 anuncios visibles simultaneamente en viewport
|
||||||
|
- UX: Mantener legibilidad del contenido
|
||||||
|
- Performance: No afectar tiempos de carga (lazy load existente)
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
- Incrementar ubicaciones potenciales de anuncios de ~8 a ~15-20
|
||||||
|
- Proporcionar control granular por tipo de elemento
|
||||||
|
- Mantener cumplimiento con politicas de AdSense
|
||||||
|
- Mejorar ingresos sin sacrificar UX drasticamente
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
- No implementar insercion dentro de parrafos (mid-paragraph)
|
||||||
|
- No implementar anuncios de video
|
||||||
|
- No cambiar el sistema de delay/lazy load existente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Decision 1: Tipos de ubicacion soportados
|
||||||
|
|
||||||
|
**Seleccionados:**
|
||||||
|
- Despues de parrafos (existente, mejorado)
|
||||||
|
- Despues de encabezados H2
|
||||||
|
- Despues de encabezados H3
|
||||||
|
- Despues de imagenes/figuras
|
||||||
|
- Despues de blockquotes
|
||||||
|
- Despues de listas (ul/ol completadas)
|
||||||
|
- Despues de tablas
|
||||||
|
|
||||||
|
**Rationale:** Estos elementos representan pausas naturales en la lectura donde un anuncio es menos intrusivo.
|
||||||
|
|
||||||
|
### Decision 2: Sistema de prioridades (CORREGIDO)
|
||||||
|
|
||||||
|
```
|
||||||
|
Prioridad (valores fijos, no configurables):
|
||||||
|
| Tipo | Prioridad | Justificacion |
|
||||||
|
|-------------------|-----------|----------------------------------|
|
||||||
|
| Despues de H2 | 10 | Ruptura tematica mayor |
|
||||||
|
| Despues de parrafos | 8 | Ubicacion tradicional, probada |
|
||||||
|
| Despues de H3 | 7 | Ruptura tematica menor |
|
||||||
|
| Despues de imagenes | 6 | Pausa visual natural |
|
||||||
|
| Despues de listas | 5 | Fin de enumeracion |
|
||||||
|
| Despues de blockquotes | 4 | Fin de cita |
|
||||||
|
| Despues de tablas | 3 | Fin de datos tabulares |
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nota:** El orden numerico refleja la prioridad real. H2 > parrafos > H3 > imagenes.
|
||||||
|
|
||||||
|
### Decision 3: Modos de densidad
|
||||||
|
|
||||||
|
| Modo | Max Ads | Espaciado Min | Ubicaciones Activas por Defecto |
|
||||||
|
|------|---------|---------------|--------------------------------|
|
||||||
|
| Legacy | (usa config anterior) | (usa config anterior) | Solo parrafos |
|
||||||
|
| Conservador | 5 | 5 elementos | H2, parrafos |
|
||||||
|
| Balanceado | 8 | 3 elementos | H2, H3, parrafos, imagenes |
|
||||||
|
| Agresivo | 15 | 2 elementos | Todas |
|
||||||
|
| Personalizado | Configurable | Configurable | Configurable |
|
||||||
|
|
||||||
|
### Decision 4: Estrategia de campos legacy
|
||||||
|
|
||||||
|
**Problema:** Existen campos en el grupo `behavior` que se solapan con los nuevos:
|
||||||
|
|
||||||
|
| Campo Legacy | Campo Nuevo | Estrategia |
|
||||||
|
|--------------|-------------|------------|
|
||||||
|
| post_content_enabled | N/A | Se mantiene para modo legacy |
|
||||||
|
| post_content_max_ads (1-8) | incontent_max_total_ads (1-15) | Deprecacion suave |
|
||||||
|
| post_content_min_paragraphs_between | incontent_min_spacing | Deprecacion suave |
|
||||||
|
| post_content_random_mode | Probabilidades por tipo | Mapeo: true → 75% |
|
||||||
|
| post_content_after_paragraphs | N/A | Solo aplica en modo legacy |
|
||||||
|
|
||||||
|
**Solucion elegida:** Deprecacion suave con modo "legacy"
|
||||||
|
- Si `incontent_mode == "legacy"`: usar campos del grupo `behavior`
|
||||||
|
- Si `incontent_mode != "legacy"`: usar campos de `incontent_advanced`
|
||||||
|
- Mostrar banner de migracion en admin
|
||||||
|
|
||||||
|
### Decision 5: Enfoque de parsing HTML
|
||||||
|
|
||||||
|
**Opcion A: DOMDocument**
|
||||||
|
```php
|
||||||
|
$dom = new DOMDocument();
|
||||||
|
$dom->loadHTML(mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8'));
|
||||||
|
```
|
||||||
|
- Pros: Parsing robusto, manejo correcto de anidamiento
|
||||||
|
- Contras: Puede modificar HTML, mas lento
|
||||||
|
|
||||||
|
**Opcion B: Regex multiple (SELECCIONADA)**
|
||||||
|
```php
|
||||||
|
preg_split('/(<\/(?:p|h[2-3]|figure|ul|ol|table|blockquote)>)/i', $content, -1, PREG_SPLIT_DELIM_CAPTURE)
|
||||||
|
```
|
||||||
|
- Pros: Rapido, no modifica HTML
|
||||||
|
- Contras: No detecta contexto de anidamiento
|
||||||
|
|
||||||
|
**Justificacion:** Regex es suficiente para el caso de uso. El contexto de `<img>` dentro de `<figure>` se resuelve con validacion adicional.
|
||||||
|
|
||||||
|
### Decision 6: Definicion de "Elemento de Bloque Contable"
|
||||||
|
|
||||||
|
Para efectos de espaciado y conteo, un "elemento" se define como:
|
||||||
|
|
||||||
|
| Tag | Cuenta | Notas |
|
||||||
|
|-----|--------|-------|
|
||||||
|
| `</p>` | SI | Parrafo |
|
||||||
|
| `</h2>`, `</h3>` | SI | Encabezados (H4 no soportado) |
|
||||||
|
| `</figure>` | SI | Contenedor de imagen |
|
||||||
|
| `</ul>`, `</ol>` | SI | Listas (contenedor, no items) |
|
||||||
|
| `</table>` | SI | Tabla (contenedor) |
|
||||||
|
| `</blockquote>` | SI | Cita en bloque |
|
||||||
|
| `<img>` standalone | SI | Solo si NO esta dentro de `<figure>` |
|
||||||
|
| `</li>`, `</tr>`, `</td>` | NO | Elementos internos |
|
||||||
|
| `</div>` | NO | Divs genericos |
|
||||||
|
|
||||||
|
### Decision 7: Algoritmo de insercion
|
||||||
|
|
||||||
|
El algoritmo sigue 6 pasos secuenciales:
|
||||||
|
|
||||||
|
```
|
||||||
|
PASO 1: ESCANEO
|
||||||
|
→ Detectar todos los tags de cierre de bloques contables
|
||||||
|
→ Registrar: {posicion, tipo, indice}
|
||||||
|
|
||||||
|
PASO 2: FILTRADO POR CONFIGURACION
|
||||||
|
→ Eliminar ubicaciones con enabled=false
|
||||||
|
|
||||||
|
PASO 3: PROBABILIDAD DETERMINISTICA
|
||||||
|
→ Seed: crc32(post_id . date('Y-m-d'))
|
||||||
|
→ mt_srand(seed) + mt_rand(1, 100)
|
||||||
|
→ Eliminar si rand > probabilidad
|
||||||
|
|
||||||
|
PASO 4: FILTRADO POR ESPACIADO
|
||||||
|
→ Iterar en orden DOM
|
||||||
|
→ Eliminar si distancia < min_spacing
|
||||||
|
|
||||||
|
PASO 5: LIMITE Y PRIORIDAD
|
||||||
|
→ Si count > max_total_ads:
|
||||||
|
→ Ordenar por prioridad DESC
|
||||||
|
→ Tomar primeros N
|
||||||
|
→ Reordenar por posicion DOM
|
||||||
|
|
||||||
|
PASO 6: INSERCION
|
||||||
|
→ Insertar HTML de ad despues de cada tag
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decision 8: Probabilidad deterministica
|
||||||
|
|
||||||
|
**Problema:** `rand()` genera posiciones diferentes en cada request, afectando cache.
|
||||||
|
|
||||||
|
**Solucion:**
|
||||||
|
```php
|
||||||
|
$seed = crc32($post_id . date('Y-m-d'));
|
||||||
|
mt_srand($seed);
|
||||||
|
// mt_rand() ahora es determinístico por día
|
||||||
|
```
|
||||||
|
|
||||||
|
**Beneficios:**
|
||||||
|
- Mismo post = mismas posiciones durante el dia
|
||||||
|
- Cache de pagina funciona correctamente
|
||||||
|
- Al dia siguiente, posiciones cambian (variedad)
|
||||||
|
|
||||||
|
### Decision 9: Estrategia de seleccion configurable
|
||||||
|
|
||||||
|
**Problema:** El orden entre espaciado y prioridad afecta qué ubicaciones sobreviven cuando hay conflictos.
|
||||||
|
|
||||||
|
**Solucion:** Campo `incontent_priority_mode` con dos opciones:
|
||||||
|
|
||||||
|
| Modo | Orden de pasos | Resultado |
|
||||||
|
|------|----------------|-----------|
|
||||||
|
| `position` | Espaciado → Prioridad | Distribucion uniforme, respeta orden DOM |
|
||||||
|
| `priority` | Prioridad → Espaciado | Maximiza valor, H2/H3 siempre ganan |
|
||||||
|
|
||||||
|
**Algoritmo segun modo:**
|
||||||
|
|
||||||
|
```
|
||||||
|
SI incontent_priority_mode == "position":
|
||||||
|
PASO 4: Filtrar por espaciado (orden DOM)
|
||||||
|
PASO 5: Ordenar por prioridad, tomar max_total_ads
|
||||||
|
|
||||||
|
SI incontent_priority_mode == "priority":
|
||||||
|
PASO 4: Ordenar por prioridad DESC
|
||||||
|
PASO 5: Iterar en orden de prioridad, eliminar si viola espaciado
|
||||||
|
Tomar max_total_ads
|
||||||
|
```
|
||||||
|
|
||||||
|
**Default:** `position` (comportamiento mas predecible y uniforme)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estructura de UI (FormBuilder)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="card shadow-sm mb-3" style="border-left: 4px solid #0d6efd;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="fw-bold mb-3">
|
||||||
|
<i class="bi bi-body-text me-2"></i>
|
||||||
|
In-Content Ads Avanzado
|
||||||
|
<span class="badge bg-success ms-2">Nuevo</span>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<!-- Indicador de densidad -->
|
||||||
|
<div id="densityIndicator" class="alert alert-info small mb-3">
|
||||||
|
Densidad estimada: <strong>Media</strong> <span class="badge bg-warning">~6 ads</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selector de modo -->
|
||||||
|
<select class="form-select mb-4" name="incontent_mode">
|
||||||
|
<option value="legacy">Legacy (config anterior)</option>
|
||||||
|
<option value="conservative">Conservador</option>
|
||||||
|
<option value="balanced" selected>Balanceado</option>
|
||||||
|
<option value="aggressive">Agresivo</option>
|
||||||
|
<option value="custom">Personalizado</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Subseccion: Ubicaciones -->
|
||||||
|
<details class="mb-3 border rounded" open>
|
||||||
|
<summary class="p-3 bg-light fw-bold">Ubicaciones por Elemento</summary>
|
||||||
|
<div class="p-3">
|
||||||
|
<!-- Toggle + probabilidad por tipo -->
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Subseccion: Limites -->
|
||||||
|
<details class="mb-3 border rounded">
|
||||||
|
<summary class="p-3 bg-light fw-bold">Limites y Espaciado</summary>
|
||||||
|
<div class="p-3">
|
||||||
|
<!-- max_total_ads, min_spacing -->
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Warning densidad alta -->
|
||||||
|
<div id="highDensityWarning" class="alert alert-warning small d-none">
|
||||||
|
Densidad alta puede afectar UX y violar politicas de AdSense.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schema JSON Completo
|
||||||
|
|
||||||
|
**Nota sobre formato de options:**
|
||||||
|
- **Array** `["25", "50", "75", "100"]`: Cuando value y label son identicos
|
||||||
|
- **Objeto** `{"2": "2 elementos"}`: Cuando label difiere del value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"incontent_advanced": {
|
||||||
|
"label": "In-Content Ads Avanzado",
|
||||||
|
"priority": 69,
|
||||||
|
"fields": {
|
||||||
|
"incontent_mode": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Modo de densidad",
|
||||||
|
"default": "legacy",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"legacy": "Legacy (config anterior)",
|
||||||
|
"conservative": "Conservador (max 5, espaciado 5)",
|
||||||
|
"balanced": "Balanceado (max 8, espaciado 3)",
|
||||||
|
"aggressive": "Agresivo (max 15, espaciado 2)",
|
||||||
|
"custom": "Personalizado"
|
||||||
|
},
|
||||||
|
"description": "Presets que ajustan limites y ubicaciones. Legacy usa campos del grupo Ubicaciones en Posts."
|
||||||
|
},
|
||||||
|
"incontent_after_h2_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de H2",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de encabezados H2"
|
||||||
|
},
|
||||||
|
"incontent_after_h2_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad H2",
|
||||||
|
"default": "100",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"],
|
||||||
|
"description": "Porcentaje de probabilidad de insercion"
|
||||||
|
},
|
||||||
|
"incontent_after_h3_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de H3",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de encabezados H3"
|
||||||
|
},
|
||||||
|
"incontent_after_h3_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad H3",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_paragraphs_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de parrafos",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de parrafos"
|
||||||
|
},
|
||||||
|
"incontent_after_paragraphs_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad parrafos",
|
||||||
|
"default": "75",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_images_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de imagenes",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar despues de figure o img standalone"
|
||||||
|
},
|
||||||
|
"incontent_after_images_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad imagenes",
|
||||||
|
"default": "75",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_lists_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de listas",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar despues de ul/ol (minimo 3 items)"
|
||||||
|
},
|
||||||
|
"incontent_after_lists_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad listas",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_blockquotes_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de blockquotes",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar despues de citas en bloque"
|
||||||
|
},
|
||||||
|
"incontent_after_blockquotes_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad blockquotes",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_tables_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de tablas",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar despues de tablas"
|
||||||
|
},
|
||||||
|
"incontent_after_tables_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad tablas",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_max_total_ads": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Maximo total de ads",
|
||||||
|
"default": "8",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"],
|
||||||
|
"description": "Cantidad maxima de anuncios in-content por post"
|
||||||
|
},
|
||||||
|
"incontent_min_spacing": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Espaciado minimo",
|
||||||
|
"default": "3",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"2": "2 elementos",
|
||||||
|
"3": "3 elementos",
|
||||||
|
"4": "4 elementos",
|
||||||
|
"5": "5 elementos",
|
||||||
|
"6": "6 elementos"
|
||||||
|
},
|
||||||
|
"description": "Minimo de elementos de bloque entre anuncios"
|
||||||
|
},
|
||||||
|
"incontent_format": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Formato de ads",
|
||||||
|
"default": "in-article",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"in-article": "In-Article (fluid)",
|
||||||
|
"auto": "Auto (responsive)"
|
||||||
|
},
|
||||||
|
"description": "Formato de anuncio para ubicaciones in-content"
|
||||||
|
},
|
||||||
|
"incontent_priority_mode": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Estrategia de seleccion",
|
||||||
|
"default": "position",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"position": "Por posicion (distribucion uniforme)",
|
||||||
|
"priority": "Por prioridad (maximizar H2/H3)"
|
||||||
|
},
|
||||||
|
"description": "Como resolver conflictos cuando dos ubicaciones estan muy cerca. 'Por posicion' respeta el orden del contenido. 'Por prioridad' favorece ubicaciones de mayor valor (H2 sobre parrafos)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
### Risk 1: Violacion de politicas de AdSense
|
||||||
|
- **Probabilidad**: Media
|
||||||
|
- **Impacto**: Alto (suspension de cuenta)
|
||||||
|
- **Mitigacion**: Indicador de densidad en admin, warning para >10 ads
|
||||||
|
|
||||||
|
### Risk 2: Degradacion de UX
|
||||||
|
- **Probabilidad**: Media-Alta
|
||||||
|
- **Impacto**: Medio (usuarios abandonan)
|
||||||
|
- **Mitigacion**: Modo conservador como default inicial, preview de densidad
|
||||||
|
|
||||||
|
### Risk 3: Conflicto con contenido corto
|
||||||
|
- **Probabilidad**: Media
|
||||||
|
- **Impacto**: Bajo
|
||||||
|
- **Mitigacion**: Campo existente `min_content_length` ya maneja esto
|
||||||
|
|
||||||
|
### Risk 4: Complejidad de migracion
|
||||||
|
- **Probabilidad**: Baja
|
||||||
|
- **Impacto**: Medio
|
||||||
|
- **Mitigacion**: Default "legacy" preserva comportamiento actual
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. **Fase 1 - Schema**: Agregar grupo `incontent_advanced` con default "legacy"
|
||||||
|
2. **Fase 2 - Sync**: `wp roi-theme sync-component adsense-placement`
|
||||||
|
3. **Fase 3 - FormBuilder**: Nueva UI con banner de migracion
|
||||||
|
4. **Fase 4 - Renderer**: Implementar ContentAdInjector con algoritmo de 6 pasos
|
||||||
|
5. **Fase 5 - Testing**: Validar en posts con contenido variado
|
||||||
|
6. **Fase 6 - Deploy**: Default "legacy", usuarios migran manualmente
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
- Cambiar `incontent_mode` a "legacy" restaura comportamiento anterior
|
||||||
|
- No hay cambios destructivos en BD
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. ~~¿Se deberia agregar un preview en vivo de donde apareceran los ads?~~ **Diferido a v2**
|
||||||
|
2. ~~¿Implementar A/B testing entre modos?~~ **Diferido a v2**
|
||||||
|
3. ~~¿Agregar reportes de rendimiento por tipo de ubicacion?~~ **Diferido a v2**
|
||||||
36
openspec/changes/add-advanced-incontent-ads/proposal.md
Normal file
36
openspec/changes/add-advanced-incontent-ads/proposal.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Change: Ampliar opciones de In-Content Ads para maximizar ingresos
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Los ingresos de AdSense han disminuido aproximadamente un 50% (de ~130 MXN/dia a ~65 MXN/dia). Se ha observado que se muestran significativamente menos anuncios que antes. El sistema actual de in-content ads solo inserta anuncios despues de parrafos, pero el contenido tiene muchos mas puntos de insercion potenciales (despues de encabezados H2/H3, despues de imagenes, despues de listas, etc.) que no se estan aprovechando.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
### Nuevas ubicaciones de insercion
|
||||||
|
- **ADDED** Insercion despues de encabezados H2 (configurable)
|
||||||
|
- **ADDED** Insercion despues de encabezados H3 (configurable)
|
||||||
|
- **ADDED** Insercion despues de imagenes/figuras
|
||||||
|
- **ADDED** Insercion despues de blockquotes
|
||||||
|
- **ADDED** Insercion despues de listas (ul/ol)
|
||||||
|
- **ADDED** Insercion despues de tablas
|
||||||
|
|
||||||
|
### Configuracion avanzada
|
||||||
|
- **ADDED** Cantidad maxima de ads aumentada de 8 a 15
|
||||||
|
- **ADDED** Control individual por tipo de ubicacion (activar/desactivar cada tipo)
|
||||||
|
- **ADDED** Prioridad de ubicaciones (orden de preferencia)
|
||||||
|
- **ADDED** Modo agresivo vs conservador
|
||||||
|
- **ADDED** Espaciado minimo entre cualquier tipo de ad
|
||||||
|
|
||||||
|
### UI Admin mejorada
|
||||||
|
- **MODIFIED** Seccion In-Content Ads reorganizada con subsecciones
|
||||||
|
- **ADDED** Preview visual de posibles ubicaciones
|
||||||
|
- **ADDED** Indicadores de densidad de anuncios
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Affected specs**: openspec/specs/adsense-placement (a crear)
|
||||||
|
- **Affected code**:
|
||||||
|
- `Schemas/adsense-placement.json` - Nuevos campos
|
||||||
|
- `Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php` - Nueva UI
|
||||||
|
- `Public/AdsensePlacement/Infrastructure/Services/ContentAdInjector.php` - Nueva logica de insercion
|
||||||
|
- **Expected outcome**: Incremento significativo en impresiones de anuncios, potencialmente duplicando o triplicando los ingresos actuales al aprovechar todas las oportunidades de insercion
|
||||||
@@ -0,0 +1,690 @@
|
|||||||
|
# Especificacion: AdSense Placement - In-Content Ads Avanzados
|
||||||
|
|
||||||
|
## Definiciones Tecnicas
|
||||||
|
|
||||||
|
### Definicion: Elemento de Bloque Contable
|
||||||
|
|
||||||
|
Un "elemento" para efectos de espaciado y conteo se define como el tag de cierre de los siguientes elementos de bloque principal:
|
||||||
|
|
||||||
|
| Tag | Cuenta como elemento | Notas |
|
||||||
|
|-----|---------------------|-------|
|
||||||
|
| `</p>` | SI | Parrafo |
|
||||||
|
| `</h2>` | SI | Encabezado nivel 2 |
|
||||||
|
| `</h3>` | SI | Encabezado nivel 3 |
|
||||||
|
| `</figure>` | SI | Contenedor de imagen con caption |
|
||||||
|
| `</ul>` | SI | Lista desordenada (el contenedor, no los `<li>`) |
|
||||||
|
| `</ol>` | SI | Lista ordenada (el contenedor, no los `<li>`) |
|
||||||
|
| `</table>` | SI | Tabla (el contenedor, no `<tr>` ni `<td>`) |
|
||||||
|
| `</blockquote>` | SI | Cita en bloque |
|
||||||
|
| `<img>` | SOLO si no esta dentro de `<figure>` | Imagen standalone |
|
||||||
|
| `</li>` | NO | Items de lista no cuentan individualmente |
|
||||||
|
| `</tr>`, `</td>` | NO | Elementos internos de tabla no cuentan |
|
||||||
|
| `</div>` | NO | Divs genericos no cuentan |
|
||||||
|
|
||||||
|
### Definicion: Prioridades de Ubicacion (Corregidas)
|
||||||
|
|
||||||
|
| Tipo de Ubicacion | Prioridad | Justificacion |
|
||||||
|
|-------------------|-----------|---------------|
|
||||||
|
| Despues de H2 | 10 | Ruptura tematica mayor, alta visibilidad |
|
||||||
|
| Despues de parrafos | 8 | Ubicacion tradicional, probada |
|
||||||
|
| Despues de H3 | 7 | Ruptura tematica menor |
|
||||||
|
| Despues de imagenes/figure | 6 | Pausa visual natural |
|
||||||
|
| Despues de listas | 5 | Fin de enumeracion |
|
||||||
|
| Despues de blockquotes | 4 | Fin de cita |
|
||||||
|
| Despues de tablas | 3 | Fin de datos tabulares |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Algoritmo de Insercion de Anuncios
|
||||||
|
|
||||||
|
El sistema DEBE seguir un algoritmo determinista para insertar anuncios in-content.
|
||||||
|
|
||||||
|
#### Scenario: Algoritmo completo de insercion
|
||||||
|
- **GIVEN** contenido HTML con multiples elementos de bloque
|
||||||
|
- **WHEN** se procesa el contenido para insertar ads
|
||||||
|
- **THEN** el sistema DEBE ejecutar los siguientes pasos en orden:
|
||||||
|
|
||||||
|
```
|
||||||
|
PASO 0: PRECONDICION - VALIDAR LONGITUD MINIMA
|
||||||
|
- Obtener min_content_length del grupo forms (default: 500)
|
||||||
|
- Si strlen(strip_tags($content)) < min_content_length:
|
||||||
|
- Retornar contenido sin modificar
|
||||||
|
- NO ejecutar pasos siguientes
|
||||||
|
- Esta validacion aplica a TODOS los modos (legacy y nuevo)
|
||||||
|
|
||||||
|
PASO 1: ESCANEO
|
||||||
|
- Parsear contenido usando regex: preg_split('/(<\/(?:p|h[2-3]|figure|ul|ol|table|blockquote)>)/i', ...)
|
||||||
|
- Para cada tag de cierre, registrar: {posicion, tipo, indice_elemento}
|
||||||
|
- Detectar <img> standalone usando logica de dos pasos:
|
||||||
|
1. Encontrar todos los <img> con: preg_match_all('/<img[^>]*>/i', $content, $imgs, PREG_OFFSET_CAPTURE)
|
||||||
|
2. Para cada <img> encontrado:
|
||||||
|
- Buscar si existe <figure> abierto antes sin cerrar
|
||||||
|
- Si NO hay <figure> abierto: registrar como ubicacion elegible tipo "image"
|
||||||
|
- Si SI hay <figure> abierto: ignorar (se contara con </figure>)
|
||||||
|
|
||||||
|
PASO 2: FILTRADO POR CONFIGURACION
|
||||||
|
- Eliminar ubicaciones cuyo tipo tenga enabled=false
|
||||||
|
- Ejemplo: si incontent_after_h3_enabled=false, eliminar todas las ubicaciones </h3>
|
||||||
|
|
||||||
|
PASO 3: APLICAR PROBABILIDAD DETERMINISTICA
|
||||||
|
- Calcular seed: crc32(post_id . date('Y-m-d'))
|
||||||
|
- Inicializar: mt_srand(seed)
|
||||||
|
- Para cada ubicacion restante:
|
||||||
|
- Si mt_rand(1, 100) > probabilidad_del_tipo: eliminar ubicacion
|
||||||
|
- Esto garantiza consistencia durante el mismo dia para cache
|
||||||
|
|
||||||
|
PASO 4-5: FILTRADO Y SELECCION (segun incontent_priority_mode)
|
||||||
|
|
||||||
|
SI incontent_priority_mode == "position" (default):
|
||||||
|
PASO 4: FILTRAR POR ESPACIADO (orden DOM)
|
||||||
|
- Ordenar ubicaciones por posicion en DOM
|
||||||
|
- Iterar secuencialmente:
|
||||||
|
- Si distancia a ubicacion anterior < min_spacing: eliminar ubicacion
|
||||||
|
- La distancia se mide en cantidad de elementos de bloque entre ambas
|
||||||
|
|
||||||
|
PASO 5: APLICAR LIMITE
|
||||||
|
- Si cantidad de ubicaciones > max_total_ads:
|
||||||
|
- Ordenar por prioridad descendente (H2=10 primero)
|
||||||
|
- Tomar las primeras max_total_ads ubicaciones
|
||||||
|
- Reordenar por posicion en DOM
|
||||||
|
|
||||||
|
SI incontent_priority_mode == "priority":
|
||||||
|
PASO 4: ORDENAR POR PRIORIDAD
|
||||||
|
- Ordenar ubicaciones por prioridad DESC (H2=10 primero, luego p=8, etc.)
|
||||||
|
|
||||||
|
PASO 5: FILTRAR POR ESPACIADO (orden de prioridad)
|
||||||
|
- Iterar en orden de prioridad (no DOM):
|
||||||
|
- Si la ubicacion viola min_spacing con alguna ya seleccionada: eliminar
|
||||||
|
- Si ya tenemos max_total_ads: parar
|
||||||
|
- Reordenar resultado final por posicion DOM
|
||||||
|
|
||||||
|
PASO 6: INSERCION
|
||||||
|
- Para cada ubicacion final, insertar HTML del ad despues del tag de cierre
|
||||||
|
- El ad usa el formato configurado (in-article, auto, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scenario: Seed deterministico para cache
|
||||||
|
- **GIVEN** un post con ID 12345 visitado multiples veces el mismo dia
|
||||||
|
- **WHEN** se calculan las posiciones de ads
|
||||||
|
- **THEN** las posiciones DEBEN ser identicas en todas las visitas del mismo dia
|
||||||
|
- **AND** al dia siguiente las posiciones pueden cambiar (nuevo seed)
|
||||||
|
|
||||||
|
#### Scenario: Validacion de longitud minima de contenido
|
||||||
|
- **GIVEN** `min_content_length` = 500 (campo del grupo `forms`)
|
||||||
|
- **AND** el contenido tiene 300 caracteres (sin tags HTML)
|
||||||
|
- **WHEN** se invoca el algoritmo de insercion
|
||||||
|
- **THEN** NO se ejecuta ningun paso del algoritmo
|
||||||
|
- **AND** se retorna el contenido original sin modificar
|
||||||
|
- **AND** esta validacion aplica tanto a modo "legacy" como a modos nuevos
|
||||||
|
|
||||||
|
#### Scenario: Resolucion de conflictos de posicion
|
||||||
|
- **GIVEN** un H2 seguido inmediatamente por un parrafo
|
||||||
|
- **WHEN** ambos tipos estan habilitados
|
||||||
|
- **AND** el espaciado minimo es 1
|
||||||
|
- **THEN** solo se inserta ad en la ubicacion de mayor prioridad (H2)
|
||||||
|
- **AND** el parrafo se cuenta para el espaciado pero no recibe ad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Ubicaciones de insercion por tipo de elemento
|
||||||
|
|
||||||
|
El sistema DEBE permitir insertar anuncios despues de diferentes tipos de elementos HTML estructurales, cada uno con configuracion independiente.
|
||||||
|
|
||||||
|
#### Scenario: Insercion despues de encabezados H2
|
||||||
|
- **WHEN** el campo `incontent_after_h2_enabled` es true
|
||||||
|
- **AND** el contenido tiene elementos `</h2>`
|
||||||
|
- **THEN** el sistema DEBE registrar cada `</h2>` como ubicacion elegible
|
||||||
|
- **AND** la probabilidad de insercion es controlada por `incontent_after_h2_probability`
|
||||||
|
|
||||||
|
#### Scenario: Insercion despues de parrafos
|
||||||
|
- **WHEN** el campo `incontent_after_paragraphs_enabled` es true
|
||||||
|
- **AND** el contenido tiene elementos `</p>`
|
||||||
|
- **THEN** el sistema DEBE registrar cada `</p>` como ubicacion elegible
|
||||||
|
- **AND** la probabilidad de insercion es controlada por `incontent_after_paragraphs_probability`
|
||||||
|
- **AND** esta es la ubicacion "tradicional" del sistema legacy
|
||||||
|
|
||||||
|
#### Scenario: Insercion despues de encabezados H3
|
||||||
|
- **WHEN** el campo `incontent_after_h3_enabled` es true
|
||||||
|
- **AND** el contenido tiene elementos `</h3>`
|
||||||
|
- **THEN** el sistema DEBE registrar cada `</h3>` como ubicacion elegible
|
||||||
|
- **AND** la probabilidad de insercion es controlada por `incontent_after_h3_probability`
|
||||||
|
|
||||||
|
#### Scenario: Insercion despues de imagenes
|
||||||
|
- **WHEN** el campo `incontent_after_images_enabled` es true
|
||||||
|
- **AND** el contenido tiene elementos `</figure>` o `<img>` standalone
|
||||||
|
- **THEN** el sistema DEBE registrar como ubicacion elegible
|
||||||
|
- **AND** si `<img>` esta dentro de `<figure>`, solo cuenta `</figure>` (no duplicar)
|
||||||
|
- **AND** la probabilidad es controlada por `incontent_after_images_probability`
|
||||||
|
|
||||||
|
#### Scenario: Insercion despues de listas
|
||||||
|
- **WHEN** el campo `incontent_after_lists_enabled` es true
|
||||||
|
- **AND** el contenido tiene elementos `</ul>` o `</ol>`
|
||||||
|
- **THEN** el sistema DEBE registrar cada cierre de lista como ubicacion elegible
|
||||||
|
- **AND** NO se insertara ad si la lista tiene menos de 3 `<li>` directos
|
||||||
|
- **AND** la probabilidad es controlada por `incontent_after_lists_probability`
|
||||||
|
|
||||||
|
#### Scenario: Conteo de items en listas anidadas
|
||||||
|
- **GIVEN** el siguiente contenido:
|
||||||
|
```html
|
||||||
|
<ul>
|
||||||
|
<li>Item 1</li>
|
||||||
|
<li>Item 2
|
||||||
|
<ul>
|
||||||
|
<li>Sub-item A</li>
|
||||||
|
<li>Sub-item B</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Item 3</li>
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
- **WHEN** se evalua si la lista externa es elegible para ad
|
||||||
|
- **THEN** solo se cuentan los `<li>` directos (hijos inmediatos): 3
|
||||||
|
- **AND** los `<li>` de la lista anidada NO se cuentan para la lista padre
|
||||||
|
- **AND** la lista anidada se evalua por separado (tiene 2 items, no elegible)
|
||||||
|
- **NOTA IMPLEMENTACION**: Usar `substr_count($list_content, '<li')` para contar items. Limitacion conocida: listas anidadas contaran todos los `<li>`. Esto es aceptable para v1 ya que listas anidadas son infrecuentes en el contenido del sitio.
|
||||||
|
|
||||||
|
#### Scenario: Todos los tipos deshabilitados
|
||||||
|
- **GIVEN** todos los campos `*_enabled` son false (H2, H3, paragraphs, images, lists, blockquotes, tables)
|
||||||
|
- **WHEN** se ejecuta el algoritmo de insercion
|
||||||
|
- **THEN** PASO 2 elimina todas las ubicaciones candidatas
|
||||||
|
- **AND** se retorna el contenido sin modificar
|
||||||
|
- **AND** no se insertan anuncios
|
||||||
|
|
||||||
|
#### Scenario: Insercion despues de blockquotes
|
||||||
|
- **WHEN** el campo `incontent_after_blockquotes_enabled` es true
|
||||||
|
- **AND** el contenido tiene elementos `</blockquote>`
|
||||||
|
- **THEN** el sistema DEBE registrar cada `</blockquote>` como ubicacion elegible
|
||||||
|
- **AND** la probabilidad es controlada por `incontent_after_blockquotes_probability`
|
||||||
|
|
||||||
|
#### Scenario: Insercion despues de tablas
|
||||||
|
- **WHEN** el campo `incontent_after_tables_enabled` es true
|
||||||
|
- **AND** el contenido tiene elementos `</table>`
|
||||||
|
- **THEN** el sistema DEBE registrar cada `</table>` como ubicacion elegible
|
||||||
|
- **AND** la probabilidad es controlada por `incontent_after_tables_probability`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Modos de densidad de anuncios
|
||||||
|
|
||||||
|
El sistema DEBE ofrecer modos predefinidos que controlan la cantidad y espaciado de anuncios.
|
||||||
|
|
||||||
|
#### Scenario: Modo legacy (backward compatibility)
|
||||||
|
- **WHEN** `incontent_mode` es "legacy"
|
||||||
|
- **THEN** el sistema NO usa el grupo `incontent_advanced` para el algoritmo
|
||||||
|
- **AND** usa los campos del grupo `behavior` (post_content_*)
|
||||||
|
- **AND** ejecuta la logica de insercion anterior (solo despues de parrafos)
|
||||||
|
- **AND** los campos de `incontent_advanced` se muestran deshabilitados en el UI
|
||||||
|
- **AND** se muestra banner: "Usando configuracion legacy. Migra al nuevo sistema para mas opciones."
|
||||||
|
|
||||||
|
#### Scenario: Modo conservador
|
||||||
|
- **WHEN** `incontent_mode` es "conservative"
|
||||||
|
- **THEN** el maximo de ads in-content es 5
|
||||||
|
- **AND** el espaciado minimo entre ads es 5 elementos
|
||||||
|
- **AND** solo se activan ubicaciones despues de H2 y parrafos por defecto
|
||||||
|
|
||||||
|
#### Scenario: Modo balanceado
|
||||||
|
- **WHEN** `incontent_mode` es "balanced"
|
||||||
|
- **THEN** el maximo de ads in-content es 8
|
||||||
|
- **AND** el espaciado minimo entre ads es 3 elementos
|
||||||
|
- **AND** se activan H2, H3, imagenes y parrafos por defecto
|
||||||
|
|
||||||
|
#### Scenario: Modo agresivo
|
||||||
|
- **WHEN** `incontent_mode` es "aggressive"
|
||||||
|
- **THEN** el maximo de ads in-content es 15
|
||||||
|
- **AND** el espaciado minimo entre ads es 2 elementos
|
||||||
|
- **AND** se activan todas las ubicaciones por defecto
|
||||||
|
|
||||||
|
#### Scenario: Modo personalizado
|
||||||
|
- **WHEN** `incontent_mode` es "custom"
|
||||||
|
- **THEN** el usuario puede configurar cada campo individualmente
|
||||||
|
- **AND** los valores de max/spacing no se sobreescriben al cambiar de modo
|
||||||
|
|
||||||
|
#### Scenario: Modificacion de campos en modo preset (auto-switch a custom)
|
||||||
|
- **GIVEN** `incontent_mode` es "balanced" (o cualquier preset excepto "custom" y "legacy")
|
||||||
|
- **WHEN** el usuario modifica manualmente cualquiera de estos campos:
|
||||||
|
- `incontent_max_total_ads`
|
||||||
|
- `incontent_min_spacing`
|
||||||
|
- Cualquier campo `*_enabled` o `*_probability`
|
||||||
|
- **THEN** el sistema DEBE cambiar automaticamente `incontent_mode` a "custom"
|
||||||
|
- **AND** mostrar mensaje informativo: "Modo cambiado a Personalizado"
|
||||||
|
- **AND** los valores modificados se preservan
|
||||||
|
|
||||||
|
#### Scenario: Cambio de modo preset sobreescribe valores
|
||||||
|
- **GIVEN** `incontent_mode` es "custom" con valores personalizados
|
||||||
|
- **WHEN** el usuario cambia `incontent_mode` a "balanced"
|
||||||
|
- **THEN** los valores de max_total_ads, min_spacing y flags enabled se sobreescriben con los del preset
|
||||||
|
- **AND** se muestra confirmacion antes de aplicar el cambio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Espaciado minimo entre anuncios
|
||||||
|
|
||||||
|
El sistema DEBE mantener un espaciado minimo entre cualquier par de anuncios in-content.
|
||||||
|
|
||||||
|
#### Scenario: Calculo de espaciado
|
||||||
|
- **GIVEN** dos ubicaciones candidatas para ads
|
||||||
|
- **WHEN** se evalua el espaciado entre ellas
|
||||||
|
- **THEN** el espaciado se calcula contando elementos de bloque entre ambas posiciones
|
||||||
|
- **AND** solo se cuentan los elementos definidos en "Elemento de Bloque Contable"
|
||||||
|
|
||||||
|
#### Scenario: Ejemplo de calculo de espaciado
|
||||||
|
- **GIVEN** el siguiente contenido:
|
||||||
|
```html
|
||||||
|
<h2>Titulo</h2> <!-- Ubicacion A (posible ad) -->
|
||||||
|
<p>Parrafo 1</p> <!-- Elemento 1 -->
|
||||||
|
<p>Parrafo 2</p> <!-- Elemento 2 -->
|
||||||
|
<figure><img></figure> <!-- Elemento 3 -->
|
||||||
|
<p>Parrafo 3</p> <!-- Ubicacion B (posible ad) - Elemento 4 -->
|
||||||
|
```
|
||||||
|
- **WHEN** min_spacing es 3
|
||||||
|
- **THEN** la distancia entre A y B es 4 elementos
|
||||||
|
- **AND** ambas ubicaciones pueden tener ads (4 >= 3)
|
||||||
|
|
||||||
|
#### Scenario: Espaciado insuficiente
|
||||||
|
- **GIVEN** min_spacing es 5
|
||||||
|
- **AND** solo hay 3 elementos entre dos ubicaciones candidatas
|
||||||
|
- **THEN** la segunda ubicacion se elimina de las candidatas
|
||||||
|
- **AND** se conserva la de mayor prioridad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Estrategia de seleccion configurable
|
||||||
|
|
||||||
|
El sistema DEBE permitir elegir como resolver conflictos cuando dos ubicaciones estan muy cerca.
|
||||||
|
|
||||||
|
#### Scenario: Modo position (default)
|
||||||
|
- **WHEN** `incontent_priority_mode` es "position"
|
||||||
|
- **AND** hay un H2 (prioridad 10) en posicion 3 y un parrafo (prioridad 8) en posicion 1
|
||||||
|
- **AND** min_spacing es 3
|
||||||
|
- **THEN** el parrafo en posicion 1 se selecciona primero (por orden DOM)
|
||||||
|
- **AND** el H2 en posicion 3 se elimina por violar espaciado (distancia 2 < 3)
|
||||||
|
- **AND** el resultado favorece distribucion uniforme
|
||||||
|
|
||||||
|
#### Scenario: Modo priority
|
||||||
|
- **WHEN** `incontent_priority_mode` es "priority"
|
||||||
|
- **AND** hay un H2 (prioridad 10) en posicion 3 y un parrafo (prioridad 8) en posicion 1
|
||||||
|
- **AND** min_spacing es 3
|
||||||
|
- **THEN** el H2 se selecciona primero (por mayor prioridad)
|
||||||
|
- **AND** el parrafo se elimina por violar espaciado con el H2
|
||||||
|
- **AND** el resultado maximiza ubicaciones de alto valor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Probabilidad deterministica por ubicacion
|
||||||
|
|
||||||
|
El sistema DEBE soportar probabilidad configurable que sea consistente durante el dia.
|
||||||
|
|
||||||
|
#### Scenario: Implementacion de probabilidad deterministica
|
||||||
|
- **GIVEN** un post_id y una fecha
|
||||||
|
- **WHEN** se calcula si insertar ad en una ubicacion
|
||||||
|
- **THEN** el seed es `crc32(post_id . 'YYYY-MM-DD')`
|
||||||
|
- **AND** se usa `mt_srand(seed)` antes de evaluar probabilidades
|
||||||
|
- **AND** cada ubicacion consume un `mt_rand(1, 100)` en orden de aparicion
|
||||||
|
|
||||||
|
#### Scenario: Valores de probabilidad disponibles
|
||||||
|
- **WHEN** el usuario configura probabilidad para cualquier tipo
|
||||||
|
- **THEN** los valores disponibles son: 25, 50, 75, 100
|
||||||
|
- **AND** el valor se interpreta como porcentaje
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Estrategia de campos legacy
|
||||||
|
|
||||||
|
Los campos existentes del grupo `behavior` relacionados con in-content ads DEBEN coexistir con el nuevo sistema mediante deprecacion suave.
|
||||||
|
|
||||||
|
#### Scenario: Campos legacy a deprecar
|
||||||
|
- **GIVEN** los siguientes campos existentes:
|
||||||
|
- `post_content_enabled` (behavior)
|
||||||
|
- `post_content_max_ads` (behavior)
|
||||||
|
- `post_content_after_paragraphs` (behavior)
|
||||||
|
- `post_content_min_paragraphs_between` (behavior)
|
||||||
|
- `post_content_random_mode` (behavior)
|
||||||
|
- `post_content_format` (behavior)
|
||||||
|
- **WHEN** el sistema tiene ambos grupos de campos
|
||||||
|
- **THEN** el grupo `incontent_advanced` tiene precedencia si `incontent_mode` != "legacy"
|
||||||
|
- **AND** si `incontent_mode` == "legacy", se usan los campos del grupo `behavior`
|
||||||
|
|
||||||
|
#### Scenario: Migracion automatica en UI
|
||||||
|
- **WHEN** el usuario visita el panel de AdSense por primera vez despues de la actualizacion
|
||||||
|
- **AND** tiene configuracion legacy activa (`post_content_enabled` = true)
|
||||||
|
- **THEN** se muestra un banner informativo sobre el nuevo sistema
|
||||||
|
- **AND** se ofrece boton "Migrar a nuevo sistema" que copia valores equivalentes
|
||||||
|
|
||||||
|
#### Scenario: Mapeo de campos legacy a nuevos
|
||||||
|
| Campo Legacy | Campo Nuevo | Logica de Mapeo |
|
||||||
|
|--------------|-------------|-----------------|
|
||||||
|
| post_content_max_ads | incontent_max_total_ads | Copia directa (ampliar opciones) |
|
||||||
|
| post_content_min_paragraphs_between | incontent_min_spacing | Copia directa |
|
||||||
|
| post_content_random_mode | N/A | Si true, todas las probabilidades = 75% |
|
||||||
|
| post_content_after_paragraphs | N/A | Se usa para primer ad, resto aleatorio |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: UI de In-Content Ads reorganizada
|
||||||
|
|
||||||
|
La interfaz de administracion DEBE organizarse en subsecciones claras usando elementos HTML semanticos.
|
||||||
|
|
||||||
|
#### Scenario: Estructura HTML de la seccion
|
||||||
|
- **WHEN** se renderiza el card de In-Content Ads Avanzado
|
||||||
|
- **THEN** DEBE seguir esta estructura:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="card shadow-sm mb-3" style="border-left: 4px solid #0d6efd;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="fw-bold mb-3">
|
||||||
|
<i class="bi bi-body-text me-2"></i>
|
||||||
|
In-Content Ads Avanzado
|
||||||
|
<span class="badge bg-success ms-2">Nuevo</span>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<!-- Indicador de densidad -->
|
||||||
|
<div id="densityIndicator" class="alert alert-info small mb-3">
|
||||||
|
<i class="bi bi-speedometer2 me-1"></i>
|
||||||
|
Densidad estimada: <strong>Media</strong>
|
||||||
|
<span class="badge bg-warning">~6 ads</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selector de modo -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold">Modo de densidad</label>
|
||||||
|
<select class="form-select" id="incontentMode">
|
||||||
|
<option value="legacy">Legacy (usar config anterior)</option>
|
||||||
|
<option value="conservative">Conservador (max 5, espaciado 5)</option>
|
||||||
|
<option value="balanced" selected>Balanceado (max 8, espaciado 3)</option>
|
||||||
|
<option value="aggressive">Agresivo (max 15, espaciado 2)</option>
|
||||||
|
<option value="custom">Personalizado</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subseccion: Ubicaciones por elemento -->
|
||||||
|
<details class="mb-3 border rounded" open>
|
||||||
|
<summary class="p-3 bg-light fw-bold cursor-pointer">
|
||||||
|
<i class="bi bi-geo-alt me-1"></i>
|
||||||
|
Ubicaciones por Elemento
|
||||||
|
</summary>
|
||||||
|
<div class="p-3">
|
||||||
|
<!-- Toggle + probabilidad para cada tipo -->
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" class="form-check-input" id="afterH2Enabled" checked>
|
||||||
|
<label class="form-check-label">Despues de H2</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<select class="form-select form-select-sm" id="afterH2Prob">
|
||||||
|
<option value="100" selected>100%</option>
|
||||||
|
<option value="75">75%</option>
|
||||||
|
<option value="50">50%</option>
|
||||||
|
<option value="25">25%</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- Repetir para H3, images, lists, blockquotes, tables -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Subseccion: Limites y espaciado -->
|
||||||
|
<details class="mb-3 border rounded">
|
||||||
|
<summary class="p-3 bg-light fw-bold cursor-pointer">
|
||||||
|
<i class="bi bi-sliders me-1"></i>
|
||||||
|
Limites y Espaciado
|
||||||
|
</summary>
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Maximo total de ads</label>
|
||||||
|
<select class="form-select" id="maxTotalAds">
|
||||||
|
<!-- Opciones 1-15 -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Espaciado minimo (elementos)</label>
|
||||||
|
<select class="form-select" id="minSpacing">
|
||||||
|
<option value="2">2 elementos</option>
|
||||||
|
<option value="3" selected>3 elementos</option>
|
||||||
|
<option value="4">4 elementos</option>
|
||||||
|
<option value="5">5 elementos</option>
|
||||||
|
<option value="6">6 elementos</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 mt-3">
|
||||||
|
<label class="form-label">Estrategia de seleccion</label>
|
||||||
|
<select class="form-select" id="priorityMode">
|
||||||
|
<option value="position" selected>Por posicion (distribucion uniforme)</option>
|
||||||
|
<option value="priority">Por prioridad (maximizar H2/H3)</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Como resolver conflictos cuando dos ubicaciones estan muy cerca</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Warning para densidad alta -->
|
||||||
|
<div id="highDensityWarning" class="alert alert-warning small d-none">
|
||||||
|
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||||
|
<strong>Atencion:</strong> Densidad alta puede afectar UX y violar politicas de AdSense.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scenario: Indicador de densidad dinamico
|
||||||
|
- **WHEN** el usuario modifica cualquier campo de in-content ads
|
||||||
|
- **THEN** el indicador de densidad se actualiza en tiempo real via JavaScript
|
||||||
|
- **AND** muestra estimacion basada en: max_ads * promedio_probabilidades / 100
|
||||||
|
- **AND** colores: verde (<5), amarillo (5-10), rojo (>10)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schema JSON - Campos Completos
|
||||||
|
|
||||||
|
### Grupo: incontent_advanced (priority 69)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"incontent_advanced": {
|
||||||
|
"label": "In-Content Ads Avanzado",
|
||||||
|
"priority": 69,
|
||||||
|
"fields": {
|
||||||
|
"incontent_mode": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Modo de densidad",
|
||||||
|
"default": "legacy",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"legacy": "Legacy (config anterior)",
|
||||||
|
"conservative": "Conservador",
|
||||||
|
"balanced": "Balanceado",
|
||||||
|
"aggressive": "Agresivo",
|
||||||
|
"custom": "Personalizado"
|
||||||
|
},
|
||||||
|
"description": "Presets que ajustan limites y ubicaciones automaticamente. Default 'legacy' para backward compatibility."
|
||||||
|
},
|
||||||
|
"incontent_after_h2_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de H2",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de encabezados H2"
|
||||||
|
},
|
||||||
|
"incontent_after_h2_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad H2",
|
||||||
|
"default": "100",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"],
|
||||||
|
"description": "Porcentaje de probabilidad de insercion"
|
||||||
|
},
|
||||||
|
"incontent_after_h3_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de H3",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de encabezados H3"
|
||||||
|
},
|
||||||
|
"incontent_after_h3_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad H3",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_paragraphs_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de parrafos",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de parrafos (ubicacion tradicional)"
|
||||||
|
},
|
||||||
|
"incontent_after_paragraphs_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad parrafos",
|
||||||
|
"default": "75",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"],
|
||||||
|
"description": "Porcentaje de probabilidad de insercion despues de parrafos"
|
||||||
|
},
|
||||||
|
"incontent_after_images_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de imagenes",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de figure o img standalone"
|
||||||
|
},
|
||||||
|
"incontent_after_images_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad imagenes",
|
||||||
|
"default": "75",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_lists_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de listas",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de ul/ol (minimo 3 items)"
|
||||||
|
},
|
||||||
|
"incontent_after_lists_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad listas",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_blockquotes_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de blockquotes",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de citas en bloque"
|
||||||
|
},
|
||||||
|
"incontent_after_blockquotes_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad blockquotes",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_after_tables_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Despues de tablas",
|
||||||
|
"default": false,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Insertar anuncios despues de tablas"
|
||||||
|
},
|
||||||
|
"incontent_after_tables_probability": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Probabilidad tablas",
|
||||||
|
"default": "50",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["25", "50", "75", "100"]
|
||||||
|
},
|
||||||
|
"incontent_max_total_ads": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Maximo total de ads",
|
||||||
|
"default": "8",
|
||||||
|
"editable": true,
|
||||||
|
"options": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"],
|
||||||
|
"description": "Cantidad maxima de anuncios in-content por post"
|
||||||
|
},
|
||||||
|
"incontent_min_spacing": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Espaciado minimo",
|
||||||
|
"default": "3",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"2": "2 elementos",
|
||||||
|
"3": "3 elementos",
|
||||||
|
"4": "4 elementos",
|
||||||
|
"5": "5 elementos",
|
||||||
|
"6": "6 elementos"
|
||||||
|
},
|
||||||
|
"description": "Minimo de elementos de bloque entre anuncios"
|
||||||
|
},
|
||||||
|
"incontent_format": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Formato de ads",
|
||||||
|
"default": "in-article",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"in-article": "In-Article (fluid)",
|
||||||
|
"auto": "Auto (responsive)"
|
||||||
|
},
|
||||||
|
"description": "Formato de anuncio para todas las ubicaciones in-content"
|
||||||
|
},
|
||||||
|
"incontent_priority_mode": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Estrategia de seleccion",
|
||||||
|
"default": "position",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"position": "Por posicion (distribucion uniforme)",
|
||||||
|
"priority": "Por prioridad (maximizar H2/H3)"
|
||||||
|
},
|
||||||
|
"description": "Como resolver conflictos cuando dos ubicaciones estan muy cerca"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementacion Tecnica
|
||||||
|
|
||||||
|
### Opcion de Parsing Recomendada: Regex Multiple
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Regex para detectar todos los elementos de bloque contables (H4 no soportado)
|
||||||
|
$pattern = '/(<\/(?:p|h[2-3]|figure|ul|ol|table|blockquote)>)/i';
|
||||||
|
$parts = preg_split($pattern, $content, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||||
|
|
||||||
|
// Para detectar <img> standalone (no dentro de figure)
|
||||||
|
// Procesar en segundo paso, verificando contexto
|
||||||
|
```
|
||||||
|
|
||||||
|
**Justificacion:**
|
||||||
|
- Mas rapido que DOMDocument
|
||||||
|
- No modifica el HTML original
|
||||||
|
- Suficiente para el caso de uso (no necesitamos validar anidamiento complejo)
|
||||||
|
- El contexto de `<img>` dentro de `<figure>` se resuelve verificando si hay `<figure>` abierto sin cerrar
|
||||||
|
|
||||||
|
### Diagrama de Dependencias de Tasks
|
||||||
|
|
||||||
|
```
|
||||||
|
1.1 Schema: grupo incontent_advanced ─┐
|
||||||
|
1.2 Schema: campos individuales ──────┼──> 1.3 Sync BD ──┬──> 2.x FormBuilder
|
||||||
|
│ │
|
||||||
|
│ └──> 3.x Renderer
|
||||||
|
│ │
|
||||||
|
│ v
|
||||||
|
│ 4.x Validacion
|
||||||
|
│ │
|
||||||
|
└──────────────────────> 5.x Docs
|
||||||
|
```
|
||||||
129
openspec/changes/add-advanced-incontent-ads/tasks.md
Normal file
129
openspec/changes/add-advanced-incontent-ads/tasks.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Tasks: Implementacion de In-Content Ads Avanzados
|
||||||
|
|
||||||
|
## Diagrama de Dependencias
|
||||||
|
|
||||||
|
```
|
||||||
|
1.1 ──┬──> 1.3 ──┬──> 2.x FormBuilder ──> 4.x
|
||||||
|
1.2 ──┘ │
|
||||||
|
└──> 3.x Renderer ──────> 4.x
|
||||||
|
│
|
||||||
|
v
|
||||||
|
5.x Docs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Schema JSON - Nuevos campos
|
||||||
|
|
||||||
|
**Prerequisitos:** Ninguno
|
||||||
|
|
||||||
|
- [ ] 1.1 Agregar grupo `incontent_advanced` con priority 69 al schema JSON
|
||||||
|
- [ ] 1.2 Agregar todos los campos definidos en spec:
|
||||||
|
- [ ] incontent_mode (select: legacy/conservative/balanced/aggressive/custom)
|
||||||
|
- [ ] incontent_after_h2_enabled + probability
|
||||||
|
- [ ] incontent_after_h3_enabled + probability
|
||||||
|
- [ ] incontent_after_paragraphs_enabled + probability (ubicacion tradicional)
|
||||||
|
- [ ] incontent_after_images_enabled + probability
|
||||||
|
- [ ] incontent_after_lists_enabled + probability
|
||||||
|
- [ ] incontent_after_blockquotes_enabled + probability
|
||||||
|
- [ ] incontent_after_tables_enabled + probability
|
||||||
|
- [ ] incontent_max_total_ads (1-15)
|
||||||
|
- [ ] incontent_min_spacing (2-6)
|
||||||
|
- [ ] incontent_format
|
||||||
|
- [ ] incontent_priority_mode (position/priority)
|
||||||
|
- [ ] 1.3 Sincronizar schema con BD via WP-CLI: `wp roi-theme sync-component adsense-placement`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. FormBuilder Admin - Nueva UI
|
||||||
|
|
||||||
|
**Prerequisitos:** 1.3 completado
|
||||||
|
|
||||||
|
- [ ] 2.1 Crear metodo `buildInContentAdvancedGroup()` en AdsensePlacementFormBuilder
|
||||||
|
- [ ] 2.2 Implementar indicador de densidad (HTML + logica de color)
|
||||||
|
- [ ] 2.3 Implementar selector de modo con presets
|
||||||
|
- [ ] 2.4 Crear subseccion colapsable "Ubicaciones por Elemento" usando `<details>`
|
||||||
|
- [ ] 2.4.1 Toggle + probabilidad para H2
|
||||||
|
- [ ] 2.4.2 Toggle + probabilidad para H3
|
||||||
|
- [ ] 2.4.3 Toggle + probabilidad para parrafos (ubicacion tradicional)
|
||||||
|
- [ ] 2.4.4 Toggle + probabilidad para imagenes
|
||||||
|
- [ ] 2.4.5 Toggle + probabilidad para listas
|
||||||
|
- [ ] 2.4.6 Toggle + probabilidad para blockquotes
|
||||||
|
- [ ] 2.4.7 Toggle + probabilidad para tablas
|
||||||
|
- [ ] 2.5 Crear subseccion colapsable "Limites y Espaciado"
|
||||||
|
- [ ] 2.5.1 Select max_total_ads (1-15)
|
||||||
|
- [ ] 2.5.2 Select min_spacing (2-6)
|
||||||
|
- [ ] 2.6 Agregar warning visual para densidad alta (>10 ads)
|
||||||
|
- [ ] 2.7 Agregar banner de migracion para usuarios con config legacy activa
|
||||||
|
- [ ] 2.8 Mantener seccion legacy existente (modo "legacy" la usa)
|
||||||
|
- [ ] 2.9 Agregar mapeos en AdsensePlacementFieldMapper para todos los campos nuevos:
|
||||||
|
- [ ] incontent_mode
|
||||||
|
- [ ] incontent_after_h2_enabled + probability
|
||||||
|
- [ ] incontent_after_h3_enabled + probability
|
||||||
|
- [ ] incontent_after_paragraphs_enabled + probability
|
||||||
|
- [ ] incontent_after_images_enabled + probability
|
||||||
|
- [ ] incontent_after_lists_enabled + probability
|
||||||
|
- [ ] incontent_after_blockquotes_enabled + probability
|
||||||
|
- [ ] incontent_after_tables_enabled + probability
|
||||||
|
- [ ] incontent_max_total_ads
|
||||||
|
- [ ] incontent_min_spacing
|
||||||
|
- [ ] incontent_format
|
||||||
|
- [ ] incontent_priority_mode
|
||||||
|
- [ ] 2.10 Implementar logica JavaScript:
|
||||||
|
- [ ] 2.10.1 Auto-switch a "custom" al modificar campos (con toast informativo)
|
||||||
|
- [ ] 2.10.2 Modal de confirmacion al cambiar de "custom" a preset
|
||||||
|
- [ ] 2.10.3 Actualizar indicador de densidad en tiempo real
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Renderer - Logica de insercion mejorada
|
||||||
|
|
||||||
|
**Prerequisitos:** 1.3 completado
|
||||||
|
|
||||||
|
- [ ] 3.1 Crear/modificar ContentAdInjector service con nuevo algoritmo
|
||||||
|
- [ ] 3.1.1 Implementar PASO 0: Validar min_content_length
|
||||||
|
- [ ] 3.1.2 Implementar PASO 1: Escaneo con regex multiple
|
||||||
|
```php
|
||||||
|
preg_split('/(<\/(?:p|h[2-3]|figure|ul|ol|table|blockquote)>)/i', ...)
|
||||||
|
```
|
||||||
|
- [ ] 3.1.3 Implementar deteccion de `<img>` standalone (no dentro de figure)
|
||||||
|
- [ ] 3.1.4 Implementar validacion de listas (usar substr_count, minimo 3 items)
|
||||||
|
- [ ] 3.2 Implementar PASO 2: Filtrado por configuracion (enabled flags)
|
||||||
|
- [ ] 3.3 Implementar PASO 3: Probabilidad deterministica
|
||||||
|
- [ ] 3.3.1 Calcular seed: `crc32(post_id . date('Y-m-d'))`
|
||||||
|
- [ ] 3.3.2 Usar `mt_srand(seed)` + `mt_rand(1, 100)`
|
||||||
|
- [ ] 3.4 Implementar PASO 4-5: Filtrado y seleccion (segun incontent_priority_mode)
|
||||||
|
- [ ] 3.4.1 Definir constantes de prioridad (H2=10, p=8, H3=7, img=6, lists=5, bq=4, table=3)
|
||||||
|
- [ ] 3.4.2 Implementar modo "position": espaciado primero, luego prioridad
|
||||||
|
- [ ] 3.4.3 Implementar modo "priority": prioridad primero, luego espaciado
|
||||||
|
- [ ] 3.4.4 Aplicar limite max_total_ads
|
||||||
|
- [ ] 3.4.5 Reordenar resultado final por posicion DOM
|
||||||
|
- [ ] 3.5 Implementar PASO 6: Insercion de ads
|
||||||
|
- [ ] 3.6 Implementar logica de precedencia legacy vs nuevo sistema
|
||||||
|
- [ ] 3.6.1 Si incontent_mode == "legacy", usar campos del grupo behavior
|
||||||
|
- [ ] 3.6.2 Si incontent_mode != "legacy", usar incontent_advanced
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Validacion y Testing
|
||||||
|
|
||||||
|
**Prerequisitos:** 2.x y 3.x completados
|
||||||
|
|
||||||
|
- [ ] 4.1 Ejecutar `validate-architecture.php adsense-placement`
|
||||||
|
- [ ] 4.2 Probar en post con contenido variado (H2, H3, images, lists, tables)
|
||||||
|
- [ ] 4.3 Verificar que seed deterministico funciona (mismas posiciones mismo dia)
|
||||||
|
- [ ] 4.4 Verificar que espaciado minimo se respeta
|
||||||
|
- [ ] 4.5 Verificar que limite max_total_ads se respeta
|
||||||
|
- [ ] 4.6 Verificar que modo legacy sigue funcionando
|
||||||
|
- [ ] 4.7 Verificar que delay de carga sigue funcionando
|
||||||
|
- [ ] 4.8 Probar indicador de densidad en admin
|
||||||
|
- [ ] 4.9 Verificar ambos modos de priority_mode (position vs priority)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Documentacion
|
||||||
|
|
||||||
|
**Prerequisitos:** 4.x completado
|
||||||
|
|
||||||
|
- [ ] 5.1 Crear spec.md final en `openspec/specs/adsense-placement/`
|
||||||
|
- [ ] 5.2 Archivar este change: `openspec archive add-advanced-incontent-ads`
|
||||||
274
openspec/changes/refactor-adsense-lazy-loading/design.md
Normal file
274
openspec/changes/refactor-adsense-lazy-loading/design.md
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
# Design: AdSense Lazy Loading con Intersection Observer
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### Problema Actual
|
||||||
|
|
||||||
|
El `adsense-loader.js` actual implementa un modelo "todo o nada":
|
||||||
|
|
||||||
|
1. Usuario interactua (scroll/click) O timeout 5s
|
||||||
|
2. Se carga `adsbygoogle.js` (biblioteca principal)
|
||||||
|
3. Se ejecutan TODOS los `push({})` simultaneamente
|
||||||
|
4. Google intenta llenar TODOS los slots de una vez
|
||||||
|
|
||||||
|
**Consecuencias:**
|
||||||
|
- Fill rate bajo: Google tiene limite de ads por pagina/sesion
|
||||||
|
- Slots vacios visibles: No hay inventario para todos
|
||||||
|
- Impresiones desperdiciadas: Ads below-the-fold nunca vistos
|
||||||
|
- Impacto en Core Web Vitals: Carga masiva de recursos
|
||||||
|
|
||||||
|
### Solucion Propuesta
|
||||||
|
|
||||||
|
Cambiar a modelo "por demanda con visibilidad":
|
||||||
|
|
||||||
|
1. La biblioteca `adsbygoogle.js` se carga UNA vez (primer ad visible)
|
||||||
|
2. Cada slot individual se activa al entrar en viewport
|
||||||
|
3. Slots permanecen ocultos hasta que tengan contenido
|
||||||
|
4. No hay timeout global, cada ad tiene su propio trigger
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- Mejorar fill rate cargando ads secuencialmente
|
||||||
|
- Eliminar espacios en blanco de slots vacios
|
||||||
|
- Reducir tiempo de carga inicial (menos JS ejecutado)
|
||||||
|
- Mejorar Core Web Vitals (menor TBT, mejor LCP)
|
||||||
|
- Cumplir politicas de Google AdSense
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
|
||||||
|
- Reciclar o eliminar ads ya cargados (viola politicas)
|
||||||
|
- Implementar "infinite scroll" de ads
|
||||||
|
- Cache de contenido de ads
|
||||||
|
- Prefetch de ads futuros
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Decision 1: Extension del Modulo Existente AdsensePlacement
|
||||||
|
|
||||||
|
**Razon:** Mantener Clean Architecture del proyecto. No crear modulo nuevo.
|
||||||
|
|
||||||
|
**Ubicacion de archivos:**
|
||||||
|
- Schema: `Schemas/adsense-placement.json` (nuevos campos en grupo `forms`)
|
||||||
|
- Renderer: `Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php`
|
||||||
|
- FormBuilder: `Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php`
|
||||||
|
- FieldMapper: `Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php`
|
||||||
|
- Asset Enqueuer: `Public/AdsensePlacement/Infrastructure/Services/AdsenseAssetEnqueuer.php`
|
||||||
|
- JavaScript: `Assets/Js/adsense-loader.js`
|
||||||
|
|
||||||
|
**Alternativas descartadas:**
|
||||||
|
- Crear modulo nuevo `AdsenseLazyLoading`: Viola principio de cohesion, duplica logica
|
||||||
|
|
||||||
|
### Decision 2: Usar Intersection Observer API
|
||||||
|
|
||||||
|
**Razon:** API nativa del navegador, alto rendimiento, soporte >95% global.
|
||||||
|
|
||||||
|
**Alternativas consideradas:**
|
||||||
|
- Scroll listener + getBoundingClientRect(): Mayor consumo de CPU
|
||||||
|
- requestAnimationFrame loop: Complejo, mismo resultado
|
||||||
|
- Third-party library (lozad.js): Dependencia innecesaria
|
||||||
|
|
||||||
|
### Decision 3: Ocultar slots por defecto con CSS Dinamico
|
||||||
|
|
||||||
|
**Razon:** Evitar layout shift (CLS) cuando un slot no recibe ad.
|
||||||
|
|
||||||
|
**Implementacion via CSSGeneratorService** (NO CSS estatico):
|
||||||
|
```php
|
||||||
|
// En AdsensePlacementRenderer.php
|
||||||
|
$this->cssGenerator->generate([
|
||||||
|
'.roi-ad-slot' => [
|
||||||
|
'display' => $lazyEnabled ? 'none' : 'block',
|
||||||
|
],
|
||||||
|
'.roi-ad-slot.roi-ad-filled' => [
|
||||||
|
'display' => 'block',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternativas descartadas:**
|
||||||
|
- CSS estatico en archivo: Viola arquitectura del tema
|
||||||
|
- `visibility: hidden`: Ocupa espacio, causa CLS
|
||||||
|
- `height: 0; overflow: hidden`: Hack, problemas con responsive
|
||||||
|
- Remover del DOM: Viola politicas de AdSense
|
||||||
|
|
||||||
|
### Decision 4: Criterios Concretos de Fill Detection
|
||||||
|
|
||||||
|
**Razon:** Evitar ambiguedad sobre cuando un ad "tiene contenido".
|
||||||
|
|
||||||
|
**Criterios para marcar como `roi-ad-filled`:**
|
||||||
|
1. El elemento `<ins class="adsbygoogle">` contiene al menos un hijo
|
||||||
|
2. **Y** ese hijo es un `<iframe>` O un `<div>` con contenido
|
||||||
|
3. **Y** el `<ins>` tiene `data-ad-status="filled"` (atributo que Google agrega)
|
||||||
|
|
||||||
|
**Criterios para marcar como `roi-ad-empty`:**
|
||||||
|
1. Timeout de `lazy_fill_timeout` ms ha pasado sin cumplir criterios de fill
|
||||||
|
2. **O** el `<ins>` tiene `data-ad-status="unfilled"`
|
||||||
|
|
||||||
|
**Implementacion con MutationObserver:**
|
||||||
|
```javascript
|
||||||
|
function checkAdFill(insElement) {
|
||||||
|
const status = insElement.getAttribute('data-ad-status');
|
||||||
|
if (status === 'filled') return 'filled';
|
||||||
|
if (status === 'unfilled') return 'empty';
|
||||||
|
|
||||||
|
// Fallback: verificar contenido si no hay atributo
|
||||||
|
if (insElement.children.length > 0) {
|
||||||
|
const hasIframe = insElement.querySelector('iframe');
|
||||||
|
const hasContent = insElement.querySelector('div[id]');
|
||||||
|
if (hasIframe || hasContent) return 'filled';
|
||||||
|
}
|
||||||
|
return 'pending';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decision 5: rootMargin Configurable via Schema
|
||||||
|
|
||||||
|
**Razon:** Cargar ads antes de que sean visibles para UX fluida.
|
||||||
|
|
||||||
|
**Valor por defecto:** `200px 0px` (200px arriba/abajo, 0 laterales)
|
||||||
|
|
||||||
|
**Configuracion via BD** (no window object):
|
||||||
|
- Campo `lazy_rootmargin` en schema JSON
|
||||||
|
- Leido por `AdsenseAssetEnqueuer` desde BD
|
||||||
|
- Pasado a JS via `wp_localize_script()`
|
||||||
|
|
||||||
|
### Decision 6: Configuracion Unica via Schema JSON
|
||||||
|
|
||||||
|
**Razon:** Seguir flujo de 5 fases del proyecto, evitar flags conflictivos.
|
||||||
|
|
||||||
|
**Campos nuevos en grupo `behavior` de `adsense-placement.json`:**
|
||||||
|
|
||||||
|
| Campo | Tipo | Default | Options | Descripcion |
|
||||||
|
|-------|------|---------|---------|-------------|
|
||||||
|
| `lazy_loading_enabled` | boolean | true | - | Habilitar lazy loading |
|
||||||
|
| `lazy_rootmargin` | select | "200" | 0, 100, 200, 300, 400, 500 | Pixeles de pre-carga |
|
||||||
|
| `lazy_fill_timeout` | select | "5000" | 3000, 5000, 7000, 10000 | Timeout en ms |
|
||||||
|
|
||||||
|
**Nota:** Se usa `select` en lugar de `number` porque el schema solo soporta: boolean, text, textarea, url, select, color. Los valores se parsean a entero en PHP. Ver `schema-changes.md` para definicion completa con labels.
|
||||||
|
|
||||||
|
**NO usar `window.roiAdsenseConfig`** - La configuracion viene de BD via `wp_localize_script()`:
|
||||||
|
```php
|
||||||
|
// En AdsenseAssetEnqueuer.php
|
||||||
|
wp_localize_script('adsense-loader', 'roiAdsenseConfig', [
|
||||||
|
'lazyEnabled' => (bool) $settings['lazy_loading_enabled'],
|
||||||
|
'rootMargin' => (int) $settings['lazy_rootmargin'] . 'px 0px',
|
||||||
|
'fillTimeout' => (int) $settings['lazy_fill_timeout'],
|
||||||
|
'debug' => WP_DEBUG,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decision 7: Manejo de Errores de Red
|
||||||
|
|
||||||
|
**Razon:** La biblioteca `adsbygoogle.js` puede fallar por red o bloqueo.
|
||||||
|
|
||||||
|
**Estrategia:**
|
||||||
|
1. `onerror` callback en script de biblioteca
|
||||||
|
2. Reintentar 1 vez despues de 2 segundos
|
||||||
|
3. Si falla segundo intento, marcar todos los slots como `roi-ad-error`
|
||||||
|
4. Log en consola si debug habilitado
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
newScript.onerror = function() {
|
||||||
|
if (retryCount < 1) {
|
||||||
|
retryCount++;
|
||||||
|
setTimeout(() => loadLibrary(), 2000);
|
||||||
|
} else {
|
||||||
|
markAllSlotsAsError();
|
||||||
|
debugLog('AdSense library failed to load after retry');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
### Risk 1: Ads below-the-fold nunca cargan
|
||||||
|
|
||||||
|
**Mitigacion:** `rootMargin: '200px'` pre-carga. Usuario que scrollea vera ads.
|
||||||
|
|
||||||
|
**Trade-off aceptado:** Si usuario no scrollea, no ve ads below-fold. Esto es BUENO para el anunciante (no paga por impresion no vista).
|
||||||
|
|
||||||
|
### Risk 2: Adblockers detectan Intersection Observer
|
||||||
|
|
||||||
|
**Mitigacion:** Nula. Si adblocker activo, ads no cargan de todas formas.
|
||||||
|
|
||||||
|
### Risk 3: Navegadores antiguos sin soporte
|
||||||
|
|
||||||
|
**Mitigacion:** Fallback a carga tradicional (todos al inicio).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (!('IntersectionObserver' in window)) {
|
||||||
|
// Fallback: usar modo legacy existente
|
||||||
|
loadAllAdsLegacy();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risk 4: Slots sin ad permanecen ocultos siempre
|
||||||
|
|
||||||
|
**Mitigacion:** Timeout por slot configurable. Clase `roi-ad-empty` permite styling si necesario.
|
||||||
|
|
||||||
|
### Risk 5: Race condition en carga de biblioteca
|
||||||
|
|
||||||
|
**Mitigacion:** Ya resuelto en implementacion actual con callback `onload`. Documentado para mantener.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
### Fase 1: Schema JSON
|
||||||
|
|
||||||
|
1. Agregar campos `lazy_loading_enabled`, `lazy_rootmargin`, `lazy_fill_timeout` al grupo `behavior` de `adsense-placement.json`
|
||||||
|
2. Ejecutar `wp roi-theme sync-component adsense-placement`
|
||||||
|
3. Verificar campos en BD
|
||||||
|
|
||||||
|
### Fase 2: Renderer (BD → HTML + CSS)
|
||||||
|
|
||||||
|
1. Actualizar `AdsensePlacementRenderer.php` para CSS dinamico
|
||||||
|
2. Actualizar `AdsenseAssetEnqueuer.php` para pasar config a JS
|
||||||
|
3. Actualizar `AdsensePlacementFieldMapper.php` con nuevos campos
|
||||||
|
|
||||||
|
### Fase 3: FormBuilder (UI Admin)
|
||||||
|
|
||||||
|
1. Actualizar `AdsensePlacementFormBuilder.php` con UI para nuevos campos
|
||||||
|
2. Agregar nota sobre necesidad de vaciar cache
|
||||||
|
|
||||||
|
### Fase 4: JavaScript (Infrastructure)
|
||||||
|
|
||||||
|
1. Refactorizar `adsense-loader.js` con Intersection Observer
|
||||||
|
2. Implementar MutationObserver para fill detection
|
||||||
|
3. Implementar fallback para navegadores sin soporte
|
||||||
|
4. Mantener compatibilidad con `lazy_loading_enabled: false`
|
||||||
|
|
||||||
|
### Fase 5: Validacion y Testing
|
||||||
|
|
||||||
|
1. Ejecutar validador de arquitectura
|
||||||
|
2. Probar en desarrollo con DevTools (Network throttling)
|
||||||
|
3. Verificar que ads cargan al scroll
|
||||||
|
4. Verificar que slots vacios NO se muestran
|
||||||
|
5. Medir Core Web Vitals con Lighthouse
|
||||||
|
|
||||||
|
### Post-Implementacion: Deploy y Monitoreo
|
||||||
|
|
||||||
|
1. Commit con mensaje descriptivo
|
||||||
|
2. Deploy a produccion
|
||||||
|
3. Vaciar cache (Redis, W3TC)
|
||||||
|
4. Verificar fill rate en AdSense dashboard (24-48h)
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
|
||||||
|
Si hay problemas:
|
||||||
|
1. En admin, cambiar `lazy_loading_enabled` a false
|
||||||
|
2. El sistema vuelve a modo legacy automaticamente
|
||||||
|
3. No requiere deploy de codigo
|
||||||
|
|
||||||
|
## Open Questions - RESUELTOS
|
||||||
|
|
||||||
|
1. **Cual es el rootMargin optimo?**
|
||||||
|
- **Resuelto:** 200px por defecto, configurable via admin
|
||||||
|
|
||||||
|
2. **Timeout por slot para "dar por vacio"?**
|
||||||
|
- **Resuelto:** 5000ms por defecto, configurable via admin
|
||||||
|
|
||||||
|
3. **Como detectar fill de forma confiable?**
|
||||||
|
- **Resuelto:** Usar `data-ad-status` de Google + fallback a children check
|
||||||
|
|
||||||
|
4. **Donde va la configuracion?**
|
||||||
|
- **Resuelto:** Schema JSON → BD → wp_localize_script (NO window globals)
|
||||||
54
openspec/changes/refactor-adsense-lazy-loading/proposal.md
Normal file
54
openspec/changes/refactor-adsense-lazy-loading/proposal.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Change: Refactorizar AdSense Lazy Loading con Intersection Observer
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
La implementacion actual carga TODOS los ads simultaneamente despues de interaccion del usuario o timeout de 5 segundos. Esto causa:
|
||||||
|
|
||||||
|
1. **Slots vacios visibles**: Cuando hay mas ads que inventario disponible, los slots vacios quedan visibles en la pagina creando espacios en blanco.
|
||||||
|
2. **Sobrecarga inicial**: Cargar 20+ ads simultaneamente impacta el rendimiento y el fill rate de Google.
|
||||||
|
3. **Desperdicio de impresiones**: Ads below-the-fold se cargan aunque el usuario nunca llegue a verlos.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **BREAKING**: El comportamiento de carga cambia de "cargar todo" a "cargar por visibilidad"
|
||||||
|
- Nuevos campos de configuracion en schema `adsense-placement.json` (grupo `forms`)
|
||||||
|
- Extension del modulo `AdsensePlacement` existente (NO modulo nuevo)
|
||||||
|
- Implementar Intersection Observer para detectar cuando un slot entra al viewport
|
||||||
|
- Cargar cada ad individualmente cuando el usuario se aproxima (rootMargin configurable)
|
||||||
|
- NO mostrar el contenedor `.roi-ad-slot` hasta que el ad tenga contenido real
|
||||||
|
- Estilos generados via CSSGeneratorService (NO CSS estatico)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Affected specs: Extension de especificacion existente `adsense-placement`
|
||||||
|
- Affected code:
|
||||||
|
- `Schemas/adsense-placement.json` - Nuevos campos en grupo `forms`
|
||||||
|
- `Assets/Js/adsense-loader.js` - Refactorizacion con Intersection Observer
|
||||||
|
- `Public/AdsensePlacement/Infrastructure/Ui/AdsensePlacementRenderer.php` - Ajustar markup y estilos
|
||||||
|
- `Public/AdsensePlacement/Infrastructure/Services/AdsenseAssetEnqueuer.php` - Pasar config a JS
|
||||||
|
- `Admin/AdsensePlacement/Infrastructure/Ui/AdsensePlacementFormBuilder.php` - Nuevos campos UI
|
||||||
|
- `Admin/AdsensePlacement/Infrastructure/FieldMapping/AdsensePlacementFieldMapper.php` - Mapping
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
Esta mejora se integra al modulo **existente** `AdsensePlacement`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Public/AdsensePlacement/
|
||||||
|
├── Domain/ # Sin cambios (no hay logica de negocio nueva)
|
||||||
|
├── Application/ # Sin cambios
|
||||||
|
└── Infrastructure/
|
||||||
|
├── Ui/
|
||||||
|
│ └── AdsensePlacementRenderer.php # Genera CSS dinamico via CSSGenerator
|
||||||
|
└── Services/
|
||||||
|
└── AdsenseAssetEnqueuer.php # Enqueue JS con config desde BD
|
||||||
|
|
||||||
|
Admin/AdsensePlacement/
|
||||||
|
├── Infrastructure/
|
||||||
|
│ ├── Ui/
|
||||||
|
│ │ └── AdsensePlacementFormBuilder.php # Nuevos campos lazy loading
|
||||||
|
│ └── FieldMapping/
|
||||||
|
│ └── AdsensePlacementFieldMapper.php # Mapping nuevos campos
|
||||||
|
```
|
||||||
|
|
||||||
|
**NO se crea modulo nuevo** - es extension del componente existente.
|
||||||
284
openspec/changes/refactor-adsense-lazy-loading/sanity-tests.md
Normal file
284
openspec/changes/refactor-adsense-lazy-loading/sanity-tests.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# Pruebas Sanitarias - AdSense Lazy Loading
|
||||||
|
|
||||||
|
> **Objetivo:** Verificar funcionamiento basico en navegador despues de deploy
|
||||||
|
> **Tiempo estimado:** 15-20 minutos
|
||||||
|
> **Entorno:** analisisdepreciosunitarios.com (PRODUCCION)
|
||||||
|
> **Flujo:** Local (desarrollo) → Deploy → Produccion (pruebas)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PRE-REQUISITOS
|
||||||
|
|
||||||
|
### 0. Deploy Completado
|
||||||
|
|
||||||
|
- [ ] Cambios commiteados en local
|
||||||
|
- [ ] Deploy a produccion ejecutado
|
||||||
|
- [ ] `wp roi-theme sync-component adsense-placement` ejecutado en produccion
|
||||||
|
- [ ] Cache vaciado (Redis, W3TC, Cloudflare si aplica)
|
||||||
|
|
||||||
|
### 1. Verificar Entorno Produccion
|
||||||
|
|
||||||
|
- [ ] Sitio accesible en https://analisisdepreciosunitarios.com/
|
||||||
|
- [ ] DevTools abierto (F12)
|
||||||
|
- [ ] Consola visible (para ver logs de debug)
|
||||||
|
- [ ] Network tab visible (para ver requests de AdSense)
|
||||||
|
|
||||||
|
### 2. Verificar Configuracion en BD (Produccion)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via SSH al VPS
|
||||||
|
ssh VPSContabo
|
||||||
|
cd /var/www/preciosunitarios/public_html
|
||||||
|
wp db query "SELECT setting_key, setting_value FROM wp_roi_theme_component_settings WHERE component_name = 'adsense-placement' AND setting_key LIKE '%lazy%';" --allow-root
|
||||||
|
```
|
||||||
|
|
||||||
|
**Valores esperados:**
|
||||||
|
- `lazy_loading_enabled` = `1` (o `true`)
|
||||||
|
- `lazy_rootmargin` = `200`
|
||||||
|
- `lazy_fill_timeout` = `5000`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SANITY TEST 1: Carga Inicial (Lazy Enabled)
|
||||||
|
|
||||||
|
**Tiempo:** 3 min
|
||||||
|
|
||||||
|
### Pasos:
|
||||||
|
1. Abrir DevTools > Console
|
||||||
|
2. Navegar a un articulo con ads: https://analisisdepreciosunitarios.com/analisis-de-precios-unitarios/
|
||||||
|
3. Observar consola
|
||||||
|
|
||||||
|
### Verificar:
|
||||||
|
|
||||||
|
- [ ] **ST1.1** Aparece `[AdSense Lazy] Inicializando AdSense Lazy Loader v2.0`
|
||||||
|
- [ ] **ST1.2** Aparece `[AdSense Lazy] Config: lazyEnabled=true, rootMargin=200px 0px, fillTimeout=5000`
|
||||||
|
- [ ] **ST1.3** Aparece `[AdSense Lazy] Intersection Observer inicializado`
|
||||||
|
- [ ] **ST1.4** Los slots `.roi-ad-slot` tienen `display: none` inicialmente (inspeccionar CSS)
|
||||||
|
- [ ] **ST1.5** Solo slots en viewport muestran `[AdSense Lazy] Slot entro al viewport`
|
||||||
|
|
||||||
|
### Screenshot Console:
|
||||||
|
```
|
||||||
|
Pegar screenshot de consola aqui
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SANITY TEST 2: Activacion por Scroll
|
||||||
|
|
||||||
|
**Tiempo:** 3 min
|
||||||
|
|
||||||
|
### Pasos:
|
||||||
|
1. Continuar en el mismo articulo
|
||||||
|
2. Hacer scroll lento hacia abajo
|
||||||
|
3. Observar consola mientras aparecen nuevos slots
|
||||||
|
|
||||||
|
### Verificar:
|
||||||
|
|
||||||
|
- [ ] **ST2.1** Al scrollear, nuevos mensajes `[AdSense Lazy] Slot entro al viewport`
|
||||||
|
- [ ] **ST2.2** Mensaje `[AdSense Lazy] Activando slot...` por cada slot visible
|
||||||
|
- [ ] **ST2.3** En Network tab: requests a `pagead2.googlesyndication.com` aparecen progresivamente
|
||||||
|
- [ ] **ST2.4** Slots activados reciben clase `roi-ad-filled` o `roi-ad-empty`
|
||||||
|
|
||||||
|
### Nota Fill Rate:
|
||||||
|
```
|
||||||
|
Slots activados: ___
|
||||||
|
Slots filled: ___
|
||||||
|
Slots empty: ___
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SANITY TEST 3: Deteccion de Fill
|
||||||
|
|
||||||
|
**Tiempo:** 3 min
|
||||||
|
|
||||||
|
### Pasos:
|
||||||
|
1. Inspeccionar un slot que recibio ad (clase `roi-ad-filled`)
|
||||||
|
2. Inspeccionar un slot vacio (clase `roi-ad-empty`)
|
||||||
|
|
||||||
|
### Verificar:
|
||||||
|
|
||||||
|
- [ ] **ST3.1** Slot filled tiene `display: block` (visible)
|
||||||
|
- [ ] **ST3.2** Slot empty tiene `display: none` (oculto)
|
||||||
|
- [ ] **ST3.3** Slot filled contiene `<ins>` con `data-ad-status="filled"`
|
||||||
|
- [ ] **ST3.4** Consola muestra `[AdSense Lazy] Slot marcado como filled` o `empty`
|
||||||
|
|
||||||
|
### Screenshot Slot Filled:
|
||||||
|
```
|
||||||
|
Pegar screenshot del inspector aqui
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SANITY TEST 4: Timeout de Fill
|
||||||
|
|
||||||
|
**Tiempo:** 5 min (esperar timeout)
|
||||||
|
|
||||||
|
### Pasos:
|
||||||
|
1. Bloquear requests de AdSense temporalmente:
|
||||||
|
- DevTools > Network > Click derecho en request de googlesyndication
|
||||||
|
- "Block request URL" o usar extension de bloqueo
|
||||||
|
2. Recargar pagina
|
||||||
|
3. Esperar 5 segundos (fillTimeout)
|
||||||
|
|
||||||
|
### Verificar:
|
||||||
|
|
||||||
|
- [ ] **ST4.1** Slots muestran `[AdSense Lazy] Timeout alcanzado para slot`
|
||||||
|
- [ ] **ST4.2** Slots reciben clase `roi-ad-empty`
|
||||||
|
- [ ] **ST4.3** Slots permanecen ocultos (display: none)
|
||||||
|
- [ ] **ST4.4** No hay errores JS en consola
|
||||||
|
|
||||||
|
### Desbloquear AdSense:
|
||||||
|
- [ ] Remover bloqueo de AdSense despues del test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SANITY TEST 5: Modo Legacy (Lazy Disabled)
|
||||||
|
|
||||||
|
**Tiempo:** 4 min
|
||||||
|
|
||||||
|
### Pasos:
|
||||||
|
1. Cambiar configuracion en BD (via SSH):
|
||||||
|
```bash
|
||||||
|
ssh VPSContabo
|
||||||
|
cd /var/www/preciosunitarios/public_html
|
||||||
|
wp db query "UPDATE wp_roi_theme_component_settings SET setting_value = '0' WHERE component_name = 'adsense-placement' AND setting_key = 'lazy_loading_enabled';" --allow-root
|
||||||
|
```
|
||||||
|
2. Vaciar cache:
|
||||||
|
```bash
|
||||||
|
wp cache flush --allow-root
|
||||||
|
# Si usa W3TC: wp w3-total-cache flush all --allow-root
|
||||||
|
```
|
||||||
|
3. Recargar pagina (Ctrl+Shift+R)
|
||||||
|
4. Observar consola
|
||||||
|
|
||||||
|
### Verificar:
|
||||||
|
|
||||||
|
- [ ] **ST5.1** Consola muestra `[AdSense Lazy] Config: lazyEnabled=false`
|
||||||
|
- [ ] **ST5.2** Consola muestra `[AdSense Lazy] Iniciando modo legacy`
|
||||||
|
- [ ] **ST5.3** Los slots tienen `display: block` desde inicio
|
||||||
|
- [ ] **ST5.4** Al hacer scroll o click, todos los ads cargan simultaneamente
|
||||||
|
|
||||||
|
### Restaurar (IMPORTANTE):
|
||||||
|
```bash
|
||||||
|
ssh VPSContabo
|
||||||
|
cd /var/www/preciosunitarios/public_html
|
||||||
|
wp db query "UPDATE wp_roi_theme_component_settings SET setting_value = '1' WHERE component_name = 'adsense-placement' AND setting_key = 'lazy_loading_enabled';" --allow-root
|
||||||
|
wp cache flush --allow-root
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SANITY TEST 6: Ads Dinamicos (AJAX)
|
||||||
|
|
||||||
|
**Tiempo:** 3 min
|
||||||
|
|
||||||
|
### Pasos:
|
||||||
|
1. Buscar pagina con carga dinamica de contenido (si existe)
|
||||||
|
2. O simular en consola:
|
||||||
|
```javascript
|
||||||
|
// Simular nuevo slot dinamico
|
||||||
|
var slot = document.createElement('div');
|
||||||
|
slot.className = 'roi-ad-slot';
|
||||||
|
slot.innerHTML = '<ins class="adsbygoogle" data-ad-client="ca-pub-xxx" data-ad-slot="123"></ins><script data-adsense-push type="text/plain">(adsbygoogle = window.adsbygoogle || []).push({});</script>';
|
||||||
|
document.body.appendChild(slot);
|
||||||
|
|
||||||
|
// Disparar evento
|
||||||
|
window.dispatchEvent(new Event('roi-adsense-activate'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verificar:
|
||||||
|
|
||||||
|
- [ ] **ST6.1** Consola muestra `[AdSense Lazy] Evento roi-adsense-activate recibido`
|
||||||
|
- [ ] **ST6.2** Nuevo slot es observado por Intersection Observer
|
||||||
|
- [ ] **ST6.3** No hay errores JS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SANITY TEST 7: Performance (Core Web Vitals)
|
||||||
|
|
||||||
|
**Tiempo:** 3 min
|
||||||
|
|
||||||
|
### Pasos:
|
||||||
|
1. Abrir Lighthouse en DevTools
|
||||||
|
2. Seleccionar "Performance" solamente
|
||||||
|
3. Ejecutar audit en modo "Mobile"
|
||||||
|
|
||||||
|
### Verificar:
|
||||||
|
|
||||||
|
- [ ] **ST7.1** LCP (Largest Contentful Paint) < 2.5s
|
||||||
|
- [ ] **ST7.2** FID (First Input Delay) < 100ms
|
||||||
|
- [ ] **ST7.3** CLS (Cumulative Layout Shift) < 0.1
|
||||||
|
- [ ] **ST7.4** No hay "Avoid enormous network payloads" warning por ads
|
||||||
|
|
||||||
|
### Scores:
|
||||||
|
```
|
||||||
|
Performance: ___
|
||||||
|
LCP: ___
|
||||||
|
FID: ___
|
||||||
|
CLS: ___
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RESUMEN DE EJECUCION
|
||||||
|
|
||||||
|
| Test | Resultado | Notas |
|
||||||
|
|------|-----------|-------|
|
||||||
|
| ST1: Carga Inicial | [ ] PASS / [ ] FAIL | |
|
||||||
|
| ST2: Scroll Activation | [ ] PASS / [ ] FAIL | |
|
||||||
|
| ST3: Fill Detection | [ ] PASS / [ ] FAIL | |
|
||||||
|
| ST4: Timeout | [ ] PASS / [ ] FAIL | |
|
||||||
|
| ST5: Modo Legacy | [ ] PASS / [ ] FAIL | |
|
||||||
|
| ST6: Ads Dinamicos | [ ] PASS / [ ] FAIL | |
|
||||||
|
| ST7: Performance | [ ] PASS / [ ] FAIL | |
|
||||||
|
|
||||||
|
**Tests Passed:** ___/7
|
||||||
|
**Tests Failed:** ___/7
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DECISION
|
||||||
|
|
||||||
|
- [ ] **APROBADO PARA DEPLOY** - Todos los tests pasan
|
||||||
|
- [ ] **BLOQUEADO** - Tests criticos fallan (ST1-ST4)
|
||||||
|
- [ ] **APROBADO CON OBSERVACIONES** - Tests no criticos fallan (ST5-ST7)
|
||||||
|
|
||||||
|
**Fecha:** ____________
|
||||||
|
**Ejecutor:** ____________
|
||||||
|
**Notas adicionales:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Escribir observaciones aqui
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## COMANDOS UTILES
|
||||||
|
|
||||||
|
### Ver logs de consola filtrados:
|
||||||
|
```javascript
|
||||||
|
// En consola del navegador
|
||||||
|
console.filter = '[AdSense';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verificar config actual:
|
||||||
|
```javascript
|
||||||
|
console.log(window.roiAdsenseConfig);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forzar recarga sin cache:
|
||||||
|
```
|
||||||
|
Ctrl + Shift + R (o Cmd + Shift + R en Mac)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ver slots y su estado:
|
||||||
|
```javascript
|
||||||
|
document.querySelectorAll('.roi-ad-slot').forEach((slot, i) => {
|
||||||
|
console.log(`Slot ${i}:`, {
|
||||||
|
filled: slot.classList.contains('roi-ad-filled'),
|
||||||
|
empty: slot.classList.contains('roi-ad-empty'),
|
||||||
|
display: getComputedStyle(slot).display
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
111
openspec/changes/refactor-adsense-lazy-loading/schema-changes.md
Normal file
111
openspec/changes/refactor-adsense-lazy-loading/schema-changes.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Cambios al Schema: adsense-placement.json
|
||||||
|
|
||||||
|
## Resumen
|
||||||
|
|
||||||
|
Agregar 3 campos nuevos al grupo `behavior` para configurar el lazy loading de anuncios.
|
||||||
|
|
||||||
|
**Nota:** Los campos van en grupo `behavior` (priority 70) porque configuran el comportamiento del componente, no formularios de exclusion.
|
||||||
|
|
||||||
|
## Campos a Agregar
|
||||||
|
|
||||||
|
Ubicacion: `groups.behavior.fields`
|
||||||
|
|
||||||
|
### Campo 1: lazy_loading_enabled
|
||||||
|
|
||||||
|
```json
|
||||||
|
"lazy_loading_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Lazy Loading de Anuncios",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Cargar anuncios individualmente al entrar al viewport (mejora fill rate)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Campo 2: lazy_rootmargin
|
||||||
|
|
||||||
|
```json
|
||||||
|
"lazy_rootmargin": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Pre-carga (px antes del viewport)",
|
||||||
|
"default": "200",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"0": "0px (sin pre-carga)",
|
||||||
|
"100": "100px",
|
||||||
|
"200": "200px (recomendado)",
|
||||||
|
"300": "300px",
|
||||||
|
"400": "400px",
|
||||||
|
"500": "500px"
|
||||||
|
},
|
||||||
|
"description": "Pixeles de anticipacion para iniciar carga de anuncio"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nota:** Tipo `select` en lugar de `number` porque el schema solo soporta: boolean, text, textarea, url, select, color.
|
||||||
|
|
||||||
|
### Campo 3: lazy_fill_timeout
|
||||||
|
|
||||||
|
```json
|
||||||
|
"lazy_fill_timeout": {
|
||||||
|
"type": "select",
|
||||||
|
"label": "Timeout de llenado (ms)",
|
||||||
|
"default": "5000",
|
||||||
|
"editable": true,
|
||||||
|
"options": {
|
||||||
|
"3000": "3 segundos",
|
||||||
|
"5000": "5 segundos (recomendado)",
|
||||||
|
"7000": "7 segundos",
|
||||||
|
"10000": "10 segundos"
|
||||||
|
},
|
||||||
|
"description": "Tiempo maximo para esperar contenido de Google antes de ocultar slot"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comando de Sincronizacion
|
||||||
|
|
||||||
|
Despues de actualizar el JSON:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp roi-theme sync-component adsense-placement
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version del Schema
|
||||||
|
|
||||||
|
Incrementar version de `1.4.0` a `1.5.0` para reflejar nueva funcionalidad.
|
||||||
|
|
||||||
|
## Relacion con delay_enabled
|
||||||
|
|
||||||
|
El campo `delay_enabled` (en grupo `forms`) controla si la **biblioteca** `adsbygoogle.js` se carga con retraso.
|
||||||
|
|
||||||
|
El campo `lazy_loading_enabled` (en grupo `behavior`) controla si los **slots individuales** se activan por visibilidad.
|
||||||
|
|
||||||
|
**Ambos pueden estar activos simultaneamente** - son complementarios:
|
||||||
|
- `delay_enabled: true` = biblioteca no se carga hasta interaccion/timeout
|
||||||
|
- `lazy_loading_enabled: true` = slots se activan individualmente por viewport
|
||||||
|
|
||||||
|
Si `lazy_loading_enabled: false`, el sistema usa el comportamiento actual (cargar todos los ads de una vez despues de que la biblioteca cargue).
|
||||||
|
|
||||||
|
## Interaccion con Cache
|
||||||
|
|
||||||
|
**Importante:** El CSS dinamico generado por `CSSGeneratorService` incluye `display: none` para `.roi-ad-slot` cuando lazy loading esta habilitado.
|
||||||
|
|
||||||
|
Si se cambia `lazy_loading_enabled` de true a false:
|
||||||
|
1. El CSS dinamico cambiara en el siguiente render
|
||||||
|
2. **Se DEBE vaciar cache** (Redis, W3TC, OPcache) para que el cambio surta efecto
|
||||||
|
3. Usuarios con HTML cacheado veran slots ocultos hasta que su cache expire
|
||||||
|
|
||||||
|
**Recomendacion:** Agregar nota en FormBuilder indicando que cambios requieren vaciar cache.
|
||||||
|
|
||||||
|
## Parseo de Valores en PHP
|
||||||
|
|
||||||
|
Como los campos son tipo `select` con valores string, el `AdsenseAssetEnqueuer` debe parsear:
|
||||||
|
|
||||||
|
```php
|
||||||
|
wp_localize_script('adsense-loader', 'roiAdsenseConfig', [
|
||||||
|
'lazyEnabled' => (bool) $settings['lazy_loading_enabled'],
|
||||||
|
'rootMargin' => (int) $settings['lazy_rootmargin'] . 'px 0px',
|
||||||
|
'fillTimeout' => (int) $settings['lazy_fill_timeout'],
|
||||||
|
'debug' => WP_DEBUG,
|
||||||
|
]);
|
||||||
|
```
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
# Especificacion: AdSense Lazy Loading
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define el comportamiento del sistema de carga diferida de anuncios AdSense usando Intersection Observer para cargar ads individualmente cuando entran al viewport, ocultando slots que no reciben contenido.
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Carga Individual por Visibilidad
|
||||||
|
|
||||||
|
The system MUST load each AdSense ad slot individually when it enters the viewport, NOT all at once.
|
||||||
|
|
||||||
|
#### Scenario: Slot entra al viewport por primera vez
|
||||||
|
|
||||||
|
- **WHEN** un elemento `.roi-ad-slot[data-ad-lazy="true"]` entra al viewport (considerando rootMargin)
|
||||||
|
- **THEN** el sistema DEBE ejecutar `adsbygoogle.push({})` SOLO para ese slot
|
||||||
|
- **AND** el sistema DEBE marcar el slot como "activado" para no procesarlo de nuevo
|
||||||
|
- **AND** el sistema DEBE observar el `<ins>` interno para detectar contenido
|
||||||
|
|
||||||
|
#### Scenario: Multiples slots en viewport inicial
|
||||||
|
|
||||||
|
- **GIVEN** la pagina tiene 3 slots visibles en el viewport inicial
|
||||||
|
- **WHEN** la pagina termina de cargar
|
||||||
|
- **THEN** el sistema DEBE activar los 3 slots en orden DOM (sin delay entre ellos)
|
||||||
|
- **AND** la activacion es sincrona: push() → siguiente push() inmediatamente
|
||||||
|
- **AND** el sistema NO DEBE activar slots que estan fuera del viewport
|
||||||
|
|
||||||
|
**Clarificacion:** "Secuencial" significa en orden DOM, uno tras otro sin delay artificial. NO hay setTimeout entre activaciones. El Intersection Observer dispara callbacks para todos los elementos visibles en el mismo frame.
|
||||||
|
|
||||||
|
#### Scenario: Usuario hace scroll rapido
|
||||||
|
|
||||||
|
- **GIVEN** el usuario hace scroll rapido pasando varios slots
|
||||||
|
- **WHEN** los slots entran y salen del viewport rapidamente
|
||||||
|
- **THEN** el sistema DEBE activar cada slot que entre al viewport
|
||||||
|
- **AND** el sistema NO DEBE cancelar la activacion si el slot sale del viewport
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Biblioteca Cargada Una Sola Vez
|
||||||
|
|
||||||
|
The system MUST load the `adsbygoogle.js` library only once, when the first slot is activated.
|
||||||
|
|
||||||
|
#### Scenario: Primer slot activado
|
||||||
|
|
||||||
|
- **GIVEN** la biblioteca `adsbygoogle.js` NO ha sido cargada
|
||||||
|
- **WHEN** el primer slot entra al viewport
|
||||||
|
- **THEN** el sistema DEBE cargar la biblioteca
|
||||||
|
- **AND** el sistema DEBE esperar a que la biblioteca cargue (onload callback)
|
||||||
|
- **AND** ENTONCES ejecutar el push para ese slot
|
||||||
|
|
||||||
|
#### Scenario: Slots subsecuentes
|
||||||
|
|
||||||
|
- **GIVEN** la biblioteca `adsbygoogle.js` YA fue cargada
|
||||||
|
- **WHEN** otro slot entra al viewport
|
||||||
|
- **THEN** el sistema DEBE ejecutar el push inmediatamente
|
||||||
|
- **AND** el sistema NO DEBE intentar cargar la biblioteca de nuevo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Slots Ocultos por Defecto
|
||||||
|
|
||||||
|
The system MUST hide ad slots by default and show them only when they have content.
|
||||||
|
|
||||||
|
#### Scenario: Slot en estado inicial
|
||||||
|
|
||||||
|
- **WHEN** la pagina renderiza un `.roi-ad-slot[data-ad-lazy="true"]`
|
||||||
|
- **THEN** el slot DEBE tener `display: none` via CSS dinamico
|
||||||
|
- **AND** el slot NO DEBE ocupar espacio en el layout
|
||||||
|
|
||||||
|
#### Scenario: Slot recibe contenido de Google
|
||||||
|
|
||||||
|
- **GIVEN** un slot fue activado con push()
|
||||||
|
- **WHEN** Google inyecta contenido dentro del `<ins class="adsbygoogle">`
|
||||||
|
- **THEN** el sistema DEBE agregar clase `roi-ad-filled` al slot
|
||||||
|
- **AND** el slot DEBE hacerse visible (`display: block`)
|
||||||
|
|
||||||
|
#### Scenario: Slot NO recibe contenido (timeout)
|
||||||
|
|
||||||
|
- **GIVEN** un slot fue activado con push()
|
||||||
|
- **WHEN** pasa el tiempo configurado en `lazy_fill_timeout` sin que Google inyecte contenido
|
||||||
|
- **THEN** el sistema DEBE agregar clase `roi-ad-empty` al slot
|
||||||
|
- **AND** el slot DEBE permanecer oculto
|
||||||
|
- **AND** el sistema DEBE dejar de observar ese slot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Pre-carga con rootMargin
|
||||||
|
|
||||||
|
The system MUST pre-load ads before they enter the visible viewport to ensure smooth UX.
|
||||||
|
|
||||||
|
#### Scenario: Configuracion de rootMargin
|
||||||
|
|
||||||
|
- **WHEN** se inicializa el Intersection Observer
|
||||||
|
- **THEN** DEBE usar el valor de `lazy_rootmargin` desde configuracion
|
||||||
|
- **AND** el formato DEBE ser `'{value}px 0px'`
|
||||||
|
|
||||||
|
#### Scenario: Slot dentro del rootMargin
|
||||||
|
|
||||||
|
- **GIVEN** un slot esta 150px debajo del viewport visible
|
||||||
|
- **AND** `lazy_rootmargin` es 200
|
||||||
|
- **WHEN** el Intersection Observer evalua visibilidad
|
||||||
|
- **THEN** el slot DEBE considerarse "visible" y activarse
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Deteccion de Contenido con Criterios Concretos
|
||||||
|
|
||||||
|
The system MUST use specific criteria to determine when an ad slot has been filled.
|
||||||
|
|
||||||
|
#### Scenario: Google agrega atributo data-ad-status="filled"
|
||||||
|
|
||||||
|
- **GIVEN** un slot fue activado
|
||||||
|
- **WHEN** Google agrega `data-ad-status="filled"` al `<ins>`
|
||||||
|
- **THEN** el sistema DEBE marcar inmediatamente como `roi-ad-filled`
|
||||||
|
- **AND** el sistema DEBE desconectar observadores de ese slot
|
||||||
|
|
||||||
|
#### Scenario: Google agrega atributo data-ad-status="unfilled"
|
||||||
|
|
||||||
|
- **GIVEN** un slot fue activado
|
||||||
|
- **WHEN** Google agrega `data-ad-status="unfilled"` al `<ins>`
|
||||||
|
- **THEN** el sistema DEBE marcar inmediatamente como `roi-ad-empty`
|
||||||
|
- **AND** el sistema DEBE desconectar observadores de ese slot
|
||||||
|
|
||||||
|
#### Scenario: Fallback - Google inyecta iframe sin atributo
|
||||||
|
|
||||||
|
- **GIVEN** un slot fue activado
|
||||||
|
- **AND** el `<ins>` NO tiene atributo `data-ad-status`
|
||||||
|
- **WHEN** Google agrega un `<iframe>` dentro del `<ins>`
|
||||||
|
- **THEN** el sistema DEBE marcar como `roi-ad-filled`
|
||||||
|
|
||||||
|
#### Scenario: Fallback - Google agrega div con id
|
||||||
|
|
||||||
|
- **GIVEN** un slot fue activado
|
||||||
|
- **AND** el `<ins>` NO tiene atributo `data-ad-status`
|
||||||
|
- **WHEN** Google agrega un `<div id="...">` dentro del `<ins>`
|
||||||
|
- **THEN** el sistema DEBE marcar como `roi-ad-filled`
|
||||||
|
|
||||||
|
#### Scenario: Limpieza de observadores
|
||||||
|
|
||||||
|
- **GIVEN** un slot fue marcado como `roi-ad-filled` o `roi-ad-empty`
|
||||||
|
- **WHEN** el estado final es determinado
|
||||||
|
- **THEN** el sistema DEBE desconectar el MutationObserver de ese slot
|
||||||
|
- **AND** el sistema DEBE desconectar el IntersectionObserver de ese slot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Manejo de Errores de Red
|
||||||
|
|
||||||
|
The system MUST handle network errors when loading the AdSense library.
|
||||||
|
|
||||||
|
#### Scenario: Error de carga de biblioteca - primer intento
|
||||||
|
|
||||||
|
- **GIVEN** el sistema intenta cargar `adsbygoogle.js`
|
||||||
|
- **WHEN** la carga falla (onerror)
|
||||||
|
- **THEN** el sistema DEBE esperar 2 segundos
|
||||||
|
- **AND** el sistema DEBE reintentar la carga UNA vez
|
||||||
|
|
||||||
|
#### Scenario: Error de carga de biblioteca - segundo intento fallido
|
||||||
|
|
||||||
|
- **GIVEN** el primer intento de carga fallo
|
||||||
|
- **AND** el segundo intento tambien falla
|
||||||
|
- **WHEN** el onerror se dispara por segunda vez
|
||||||
|
- **THEN** el sistema DEBE marcar TODOS los slots como `roi-ad-error`
|
||||||
|
- **AND** el sistema DEBE registrar error en consola si debug habilitado
|
||||||
|
- **AND** el sistema NO DEBE intentar mas recargas
|
||||||
|
|
||||||
|
#### Scenario: Slots permanecen ocultos tras error
|
||||||
|
|
||||||
|
- **GIVEN** la biblioteca fallo en cargar
|
||||||
|
- **WHEN** los slots tienen clase `roi-ad-error`
|
||||||
|
- **THEN** los slots DEBEN permanecer ocultos
|
||||||
|
- **AND** NO DEBEN mostrar espacios vacios en la pagina
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Fallback para Navegadores Sin Soporte
|
||||||
|
|
||||||
|
The system MUST provide fallback for browsers without Intersection Observer support.
|
||||||
|
|
||||||
|
#### Scenario: Navegador sin Intersection Observer
|
||||||
|
|
||||||
|
- **GIVEN** `window.IntersectionObserver` es undefined
|
||||||
|
- **WHEN** el script se inicializa
|
||||||
|
- **THEN** el sistema DEBE usar el modo legacy (cargar todos despues de interaccion/timeout)
|
||||||
|
- **AND** el sistema DEBE registrar un mensaje de debug indicando fallback
|
||||||
|
|
||||||
|
#### Scenario: Navegador con soporte parcial
|
||||||
|
|
||||||
|
- **GIVEN** el navegador soporta Intersection Observer pero no MutationObserver
|
||||||
|
- **WHEN** el script se inicializa
|
||||||
|
- **THEN** el sistema DEBE usar Intersection Observer para activacion
|
||||||
|
- **AND** el sistema DEBE usar timeout fijo para determinar fill (sin deteccion dinamica)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Compatibilidad con Ads Dinamicos
|
||||||
|
|
||||||
|
The system MUST support ads injected dynamically after page load.
|
||||||
|
|
||||||
|
#### Scenario: Contenido cargado via AJAX
|
||||||
|
|
||||||
|
- **GIVEN** la pagina carga contenido adicional via AJAX con nuevos slots
|
||||||
|
- **WHEN** el evento `roi-adsense-activate` es disparado
|
||||||
|
- **THEN** el sistema DEBE buscar nuevos slots `.roi-ad-slot[data-ad-lazy="true"]` no observados
|
||||||
|
- **AND** el sistema DEBE agregarlos al Intersection Observer
|
||||||
|
|
||||||
|
#### Scenario: Infinite scroll
|
||||||
|
|
||||||
|
- **GIVEN** la pagina implementa infinite scroll
|
||||||
|
- **WHEN** nuevos slots son agregados al DOM
|
||||||
|
- **THEN** el sistema DEBE detectarlos automaticamente (MutationObserver en body)
|
||||||
|
- **OR** esperar evento `roi-adsense-activate` para procesarlos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Configuracion desde Base de Datos
|
||||||
|
|
||||||
|
The system MUST read configuration from database via wp_localize_script, NOT from hardcoded values.
|
||||||
|
|
||||||
|
#### Scenario: Configuracion disponible en JS
|
||||||
|
|
||||||
|
- **WHEN** el script `adsense-loader.js` se ejecuta
|
||||||
|
- **THEN** DEBE leer configuracion de `window.roiAdsenseConfig`
|
||||||
|
- **AND** los valores DEBEN incluir:
|
||||||
|
- `lazyEnabled` (boolean) - desde campo `lazy_loading_enabled`
|
||||||
|
- `rootMargin` (string) - desde campo `lazy_rootmargin` + 'px 0px'
|
||||||
|
- `fillTimeout` (number) - desde campo `lazy_fill_timeout`
|
||||||
|
- `debug` (boolean) - desde WP_DEBUG
|
||||||
|
|
||||||
|
#### Scenario: Modo lazy deshabilitado
|
||||||
|
|
||||||
|
- **GIVEN** `roiAdsenseConfig.lazyEnabled` es false
|
||||||
|
- **WHEN** el script se inicializa
|
||||||
|
- **THEN** el sistema DEBE usar el modo legacy (cargar todos al inicio)
|
||||||
|
- **AND** los slots DEBEN ser visibles por defecto (sin display:none)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: No Manipular Ads Cargados
|
||||||
|
|
||||||
|
The system MUST NOT remove, recycle, or manipulate ads after they are loaded.
|
||||||
|
|
||||||
|
#### Scenario: Usuario scrollea pasando un ad
|
||||||
|
|
||||||
|
- **GIVEN** un ad fue cargado y mostrado
|
||||||
|
- **WHEN** el usuario scrollea y el ad sale del viewport
|
||||||
|
- **THEN** el sistema NO DEBE remover el ad del DOM
|
||||||
|
- **AND** el sistema NO DEBE ocultar el ad
|
||||||
|
- **AND** el sistema NO DEBE intentar "reciclar" el slot
|
||||||
|
|
||||||
|
#### Scenario: Ad permanece en pagina
|
||||||
|
|
||||||
|
- **GIVEN** un ad fue cargado exitosamente
|
||||||
|
- **WHEN** la sesion del usuario continua
|
||||||
|
- **THEN** el ad DEBE permanecer en su posicion original
|
||||||
|
- **AND** el ad DEBE mantener su contenido intacto
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Logging de Debug Condicional
|
||||||
|
|
||||||
|
The system MUST provide debug logging only when enabled via WP_DEBUG.
|
||||||
|
|
||||||
|
#### Scenario: Debug habilitado
|
||||||
|
|
||||||
|
- **GIVEN** `roiAdsenseConfig.debug` es true
|
||||||
|
- **WHEN** ocurre cualquier evento significativo
|
||||||
|
- **THEN** el sistema DEBE registrar en console.log con prefijo `[AdSense Lazy]`
|
||||||
|
- **AND** los eventos incluyen: inicializacion, activacion de slot, deteccion de fill, timeout, error
|
||||||
|
|
||||||
|
#### Scenario: Debug deshabilitado
|
||||||
|
|
||||||
|
- **GIVEN** `roiAdsenseConfig.debug` es false
|
||||||
|
- **WHEN** el script ejecuta
|
||||||
|
- **THEN** el sistema NO DEBE generar output en consola
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: CSS Generado Dinamicamente
|
||||||
|
|
||||||
|
The system MUST generate CSS via CSSGeneratorService, NOT static CSS files.
|
||||||
|
|
||||||
|
#### Scenario: Lazy loading habilitado
|
||||||
|
|
||||||
|
- **GIVEN** `lazy_loading_enabled` es true en BD
|
||||||
|
- **WHEN** `AdsensePlacementRenderer` genera output
|
||||||
|
- **THEN** DEBE usar `CSSGeneratorService` para generar:
|
||||||
|
- `.roi-ad-slot { display: none }`
|
||||||
|
- `.roi-ad-slot.roi-ad-filled { display: block }`
|
||||||
|
- `.roi-ad-slot.roi-ad-empty { display: none }`
|
||||||
|
|
||||||
|
#### Scenario: Lazy loading deshabilitado
|
||||||
|
|
||||||
|
- **GIVEN** `lazy_loading_enabled` es false en BD
|
||||||
|
- **WHEN** `AdsensePlacementRenderer` genera output
|
||||||
|
- **THEN** NO DEBE agregar `display: none` a `.roi-ad-slot`
|
||||||
|
- **AND** los slots DEBEN ser visibles por defecto
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Integracion con Schema JSON
|
||||||
|
|
||||||
|
The system MUST store lazy loading configuration in the existing adsense-placement.json schema.
|
||||||
|
|
||||||
|
#### Scenario: Campos en grupo behavior
|
||||||
|
|
||||||
|
- **WHEN** el schema `adsense-placement.json` es leido
|
||||||
|
- **THEN** el grupo `behavior` DEBE contener:
|
||||||
|
- `lazy_loading_enabled` (boolean, default: true)
|
||||||
|
- `lazy_rootmargin` (select, default: "200")
|
||||||
|
- `lazy_fill_timeout` (select, default: "5000")
|
||||||
|
|
||||||
|
#### Scenario: Sincronizacion a BD
|
||||||
|
|
||||||
|
- **WHEN** se ejecuta `wp roi-theme sync-component adsense-placement`
|
||||||
|
- **THEN** los campos de lazy loading DEBEN crearse en BD
|
||||||
|
- **AND** los valores default DEBEN aplicarse si no existen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Accesibilidad de Slots Ocultos
|
||||||
|
|
||||||
|
The system MUST ensure hidden ad slots do not interfere with assistive technologies.
|
||||||
|
|
||||||
|
#### Scenario: Slot oculto no interfiere con lectores de pantalla
|
||||||
|
|
||||||
|
- **GIVEN** un slot tiene `display: none` (estado inicial o roi-ad-empty)
|
||||||
|
- **WHEN** un lector de pantalla procesa la pagina
|
||||||
|
- **THEN** el slot NO DEBE ser anunciado ni navegable
|
||||||
|
- **AND** el contenido oculto NO DEBE aparecer en el arbol de accesibilidad
|
||||||
|
|
||||||
|
#### Scenario: Slot visible es accesible
|
||||||
|
|
||||||
|
- **GIVEN** un slot fue marcado como `roi-ad-filled`
|
||||||
|
- **WHEN** el slot se hace visible (`display: block`)
|
||||||
|
- **THEN** el contenido del ad DEBE ser accesible para lectores de pantalla
|
||||||
|
- **AND** el iframe de Google conserva su propia accesibilidad
|
||||||
|
|
||||||
|
**Nota tecnica:** `display: none` automaticamente remueve elementos del arbol de accesibilidad. No se requiere `aria-hidden` adicional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Interaccion con Cache
|
||||||
|
|
||||||
|
The system MUST document cache implications when lazy loading settings change.
|
||||||
|
|
||||||
|
#### Scenario: Cambio de configuracion requiere cache flush
|
||||||
|
|
||||||
|
- **GIVEN** `lazy_loading_enabled` cambia de true a false (o viceversa)
|
||||||
|
- **WHEN** el administrador guarda la configuracion
|
||||||
|
- **THEN** el FormBuilder DEBE mostrar aviso de que se requiere vaciar cache
|
||||||
|
- **AND** el CSS dinamico cambiara en el proximo render sin cache
|
||||||
|
|
||||||
|
#### Scenario: Usuario con cache obsoleto
|
||||||
|
|
||||||
|
- **GIVEN** un usuario tiene HTML cacheado con `display: none` en slots
|
||||||
|
- **AND** el admin deshabilito lazy loading
|
||||||
|
- **WHEN** el usuario visita la pagina
|
||||||
|
- **THEN** los slots permaneceran ocultos hasta que el cache expire
|
||||||
|
- **AND** esto es comportamiento esperado (no es un bug)
|
||||||
150
openspec/changes/refactor-adsense-lazy-loading/tasks.md
Normal file
150
openspec/changes/refactor-adsense-lazy-loading/tasks.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# Tasks: Refactorizar AdSense Lazy Loading
|
||||||
|
|
||||||
|
> **Nota:** Las tareas siguen el flujo de 5 fases del proyecto. Pasos adicionales (FieldMapper, Asset Enqueuer, JS) son subtareas de infraestructura.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 1: Schema JSON
|
||||||
|
|
||||||
|
### 1.1 Actualizar adsense-placement.json
|
||||||
|
|
||||||
|
- [x] Incrementar version de `1.4.0` a `1.5.0`
|
||||||
|
- [x] Agregar campo `lazy_loading_enabled` al grupo `behavior`:
|
||||||
|
```json
|
||||||
|
"lazy_loading_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Lazy Loading de Anuncios",
|
||||||
|
"default": true,
|
||||||
|
"editable": true,
|
||||||
|
"description": "Cargar anuncios individualmente al entrar al viewport (mejora fill rate)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [x] Agregar campo `lazy_rootmargin` al grupo `behavior` (tipo select, default "200")
|
||||||
|
- [x] Agregar campo `lazy_fill_timeout` al grupo `behavior` (tipo select, default "5000")
|
||||||
|
|
||||||
|
### 1.2 Sincronizar a BD
|
||||||
|
|
||||||
|
- [x] Ejecutar `wp roi-theme sync-component adsense-placement`
|
||||||
|
- [x] Verificar campos creados en BD con valores default
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 2: Renderer (BD → HTML + CSS)
|
||||||
|
|
||||||
|
### 2.1 Actualizar AdsensePlacementRenderer.php
|
||||||
|
|
||||||
|
- [x] Leer `lazy_loading_enabled` desde settings
|
||||||
|
- [x] Generar CSS dinamico via `CSSGeneratorService`:
|
||||||
|
```php
|
||||||
|
if ($settings['lazy_loading_enabled']) {
|
||||||
|
$this->cssGenerator->generate([
|
||||||
|
'.roi-ad-slot' => ['display' => 'none'],
|
||||||
|
'.roi-ad-slot.roi-ad-filled' => ['display' => 'block'],
|
||||||
|
'.roi-ad-slot.roi-ad-empty' => ['display' => 'none'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [x] Agregar `data-ad-lazy="true"` al markup del slot si lazy enabled
|
||||||
|
- [x] Mantener compatibilidad con `lazy_loading_enabled: false`
|
||||||
|
|
||||||
|
### 2.2 Actualizar enqueue-scripts.php (AssetEnqueuer)
|
||||||
|
|
||||||
|
- [x] Leer settings de lazy loading desde BD
|
||||||
|
- [x] Usar `wp_localize_script()` para pasar config a JS:
|
||||||
|
```php
|
||||||
|
wp_localize_script('adsense-loader', 'roiAdsenseConfig', [
|
||||||
|
'lazyEnabled' => (bool) $settings['lazy_loading_enabled'],
|
||||||
|
'rootMargin' => (int) $settings['lazy_rootmargin'] . 'px 0px',
|
||||||
|
'fillTimeout' => (int) $settings['lazy_fill_timeout'],
|
||||||
|
'debug' => WP_DEBUG,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
- [x] Remover cualquier configuracion hardcodeada existente
|
||||||
|
|
||||||
|
### 2.3 Actualizar AdsensePlacementFieldMapper.php
|
||||||
|
|
||||||
|
- [x] Agregar `lazy_loading_enabled` al array de mappings
|
||||||
|
- [x] Agregar `lazy_rootmargin` al array de mappings
|
||||||
|
- [x] Agregar `lazy_fill_timeout` al array de mappings
|
||||||
|
- [x] Verificar tipos correctos (boolean, string, string → parseados en Enqueuer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 3: FormBuilder (UI Admin)
|
||||||
|
|
||||||
|
### 3.1 Actualizar AdsensePlacementFormBuilder.php
|
||||||
|
|
||||||
|
- [x] Agregar seccion "Lazy Loading" dentro del grupo Exclusions/Forms
|
||||||
|
- [x] Agregar toggle para `lazy_loading_enabled`
|
||||||
|
- [x] Agregar select para `lazy_rootmargin` (label: "Pre-carga (px)")
|
||||||
|
- [x] Agregar select para `lazy_fill_timeout` (label: "Timeout fill (ms)")
|
||||||
|
- [x] Agregar nota indicando que cambios requieren vaciar cache
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 4: JavaScript (Infrastructure)
|
||||||
|
|
||||||
|
### 4.1 Backup
|
||||||
|
|
||||||
|
- [x] Crear backup `adsense-loader.legacy.js`
|
||||||
|
|
||||||
|
### 4.2 Refactorizar adsense-loader.js
|
||||||
|
|
||||||
|
- [x] Refactorizar para leer config de `window.roiAdsenseConfig`
|
||||||
|
- [x] Implementar deteccion de soporte Intersection Observer
|
||||||
|
- [x] Implementar `observeAdSlots()` con Intersection Observer
|
||||||
|
- [x] Implementar `activateAdSlot(slot)` para activacion individual
|
||||||
|
- [x] Implementar MutationObserver para detectar contenido en `<ins>`
|
||||||
|
- [x] Implementar `checkAdFill()` con criterios concretos:
|
||||||
|
- Verificar `data-ad-status` primero
|
||||||
|
- Fallback a verificar children (iframe, div[id])
|
||||||
|
- [x] Implementar timeout por slot para marcar como vacio
|
||||||
|
- [x] Implementar manejo de error de red con retry (2s delay, max 1 retry)
|
||||||
|
- [x] Implementar fallback para navegadores sin soporte
|
||||||
|
- [x] Mantener carga diferida de `adsbygoogle.js` (primera activacion)
|
||||||
|
- [x] Mantener compatibilidad con `lazyEnabled: false` (modo legacy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FASE 5: Validacion
|
||||||
|
|
||||||
|
### 5.1 Validacion de Arquitectura
|
||||||
|
|
||||||
|
- [x] Ejecutar `php Shared/Infrastructure/Scripts/validate-architecture.php adsense-placement`
|
||||||
|
- [x] Verificar que no hay CSS estatico nuevo
|
||||||
|
- [x] Verificar que config viene de BD, no hardcodeada
|
||||||
|
- [x] Verificar que FieldMapper tiene todos los campos
|
||||||
|
|
||||||
|
### 5.2 Testing Local
|
||||||
|
|
||||||
|
- [ ] Probar con lazy_loading_enabled: true
|
||||||
|
- [ ] Verificar ads cargan al scroll (DevTools Network)
|
||||||
|
- [ ] Verificar slots vacios NO se muestran
|
||||||
|
- [ ] Probar con lazy_loading_enabled: false (modo legacy)
|
||||||
|
- [ ] Verificar fallback en navegador sin Intersection Observer
|
||||||
|
- [ ] Medir Core Web Vitals con Lighthouse (antes/despues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## POST-IMPLEMENTACION
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
- [ ] Commit con mensaje descriptivo
|
||||||
|
- [ ] Deploy a produccion
|
||||||
|
- [ ] Ejecutar sync-component en produccion
|
||||||
|
- [ ] Vaciar cache (Redis, W3TC)
|
||||||
|
- [ ] Verificar funcionamiento en produccion
|
||||||
|
|
||||||
|
### Monitoreo (24-48h)
|
||||||
|
|
||||||
|
- [ ] Monitorear fill rate en AdSense dashboard
|
||||||
|
- [ ] Verificar no hay errores en consola de usuarios
|
||||||
|
- [ ] Comparar Core Web Vitals antes/despues
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
|
||||||
|
- [ ] Remover `debug: true` de adsense-loader.js (ya pendiente)
|
||||||
|
- [ ] Remover debug de ContentAdInjector.php (ya pendiente)
|
||||||
|
- [ ] Remover `adsense-loader.legacy.js` si todo funciona (7+ dias)
|
||||||
|
- [ ] Archivar esta especificacion en `openspec/archive/`
|
||||||
1118
openspec/changes/refactor-adsense-lazy-loading/test-plan.md
Normal file
1118
openspec/changes/refactor-adsense-lazy-loading/test-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
190
openspec/specs/cache-first-architecture/spec.md
Normal file
190
openspec/specs/cache-first-architecture/spec.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# Especificacion: Hook de Pre-Evaluacion de Pagina
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define un hook que permite a plugins externos evaluar condiciones ANTES de que WordPress sirva una pagina singular. Este hook es util para plugins de control de acceso, rate limiters, membership, etc.
|
||||||
|
|
||||||
|
**Alcance del tema:** ROI-Theme SOLO provee el hook. La implementacion de logica de acceso es responsabilidad de cada plugin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Hook roi_theme_before_page_serve
|
||||||
|
|
||||||
|
The system MUST provide a hook that fires before WordPress serves a singular page, allowing external plugins to evaluate conditions and potentially redirect.
|
||||||
|
|
||||||
|
#### Scenario: Plugin externo evalua acceso antes de servir pagina
|
||||||
|
- **GIVEN** un plugin de control de acceso enganchado a `roi_theme_before_page_serve`
|
||||||
|
- **WHEN** un visitante anonimo solicita una pagina singular (post, page, CPT)
|
||||||
|
- **THEN** el tema DEBE disparar `do_action('roi_theme_before_page_serve', $post_id)`
|
||||||
|
- **AND** el hook DEBE ejecutarse en `template_redirect` con priority 0
|
||||||
|
- **AND** si el plugin llama `wp_safe_redirect()` + `exit`, la pagina NO se sirve
|
||||||
|
|
||||||
|
#### Scenario: Ningun plugin enganchado
|
||||||
|
- **GIVEN** ningun plugin esta escuchando `roi_theme_before_page_serve`
|
||||||
|
- **WHEN** un visitante solicita una pagina
|
||||||
|
- **THEN** la pagina se sirve normalmente
|
||||||
|
- **AND** no hay impacto en rendimiento
|
||||||
|
|
||||||
|
#### Scenario: Solo paginas singulares
|
||||||
|
- **GIVEN** el hook `roi_theme_before_page_serve`
|
||||||
|
- **WHEN** la solicitud es para archivo, home, search, feed, o admin
|
||||||
|
- **THEN** el hook NO DEBE dispararse
|
||||||
|
|
||||||
|
#### Scenario: Usuarios logueados excluidos
|
||||||
|
- **GIVEN** un usuario autenticado (logged in)
|
||||||
|
- **WHEN** solicita cualquier pagina
|
||||||
|
- **THEN** el hook NO DEBE dispararse
|
||||||
|
- **BECAUSE** los plugins de cache no cachean paginas para usuarios logueados
|
||||||
|
|
||||||
|
#### Scenario: REST API excluida
|
||||||
|
- **GIVEN** una peticion REST API (REST_REQUEST === true)
|
||||||
|
- **WHEN** se procesa la peticion
|
||||||
|
- **THEN** el hook NO DEBE dispararse
|
||||||
|
- **BECAUSE** las peticiones REST tienen su propio ciclo de vida y no sirven paginas HTML
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: Contexto para Plugins
|
||||||
|
|
||||||
|
The hook MUST provide sufficient context for plugins to make decisions.
|
||||||
|
|
||||||
|
#### Scenario: Plugin accede a informacion del post
|
||||||
|
- **GIVEN** un plugin enganchado a `roi_theme_before_page_serve`
|
||||||
|
- **WHEN** el hook se dispara
|
||||||
|
- **THEN** el plugin recibe `$post_id` como parametro
|
||||||
|
- **AND** `get_queried_object()` retorna el WP_Post completo
|
||||||
|
- **AND** funciones como `is_singular()`, `is_single()`, `is_page()` funcionan
|
||||||
|
|
||||||
|
#### Scenario: Plugin accede a informacion del visitante
|
||||||
|
- **GIVEN** un plugin enganchado al hook
|
||||||
|
- **WHEN** el hook se dispara
|
||||||
|
- **THEN** `$_SERVER['REMOTE_ADDR']` esta disponible
|
||||||
|
- **AND** headers HTTP estan disponibles via `$_SERVER`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: No Interferir con Cache
|
||||||
|
|
||||||
|
The theme MUST NOT define cache-blocking constants.
|
||||||
|
|
||||||
|
#### Scenario: Tema no bloquea cache
|
||||||
|
- **GIVEN** el tema roi-theme instalado
|
||||||
|
- **WHEN** plugins de cache (W3TC, WP Super Cache, etc.) estan activos
|
||||||
|
- **THEN** el tema NO DEBE definir `DONOTCACHEPAGE`
|
||||||
|
- **AND** el tema NO DEBE enviar headers `Cache-Control: no-cache`
|
||||||
|
|
||||||
|
#### Scenario: Plugin decide bloquear cache
|
||||||
|
- **GIVEN** un plugin necesita bloquear cache para una pagina
|
||||||
|
- **WHEN** el plugin esta enganchado al hook
|
||||||
|
- **THEN** es responsabilidad del PLUGIN definir `DONOTCACHEPAGE`
|
||||||
|
- **AND** el tema NO participa en esa decision
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
### Limitation 1: Page Cache Bypass
|
||||||
|
|
||||||
|
**Severity**: CRITICAL para plugins que requieren evaluacion en cada request
|
||||||
|
|
||||||
|
Cuando Page Cache esta habilitado (W3TC, WP Super Cache, etc.), el hook `roi_theme_before_page_serve` NO SE EJECUTA para paginas cacheadas.
|
||||||
|
|
||||||
|
**Razon tecnica:**
|
||||||
|
```
|
||||||
|
Request → advanced-cache.php → [Cache HIT] → HTML servido
|
||||||
|
↓
|
||||||
|
WordPress NUNCA carga
|
||||||
|
↓
|
||||||
|
Hook NUNCA se dispara
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implicacion para plugins:**
|
||||||
|
- Rate limiters: Los limites no se evaluan en cache hits
|
||||||
|
- Membership plugins: El acceso no se verifica en cache hits
|
||||||
|
- Geolocation: Las restricciones no aplican en cache hits
|
||||||
|
|
||||||
|
**Solucion:** Los plugins que requieren evaluacion en cada request deben implementar su propia estrategia (JavaScript-First, cookies, edge workers, etc.). Esto esta FUERA del alcance de esta spec.
|
||||||
|
|
||||||
|
### Limitation 2: Solo Paginas Singulares
|
||||||
|
|
||||||
|
El hook solo dispara para `is_singular() === true`. Archives, taxonomies, search, y home NO disparan el hook.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Ubicacion en Clean Architecture
|
||||||
|
|
||||||
|
El hook DEBE registrarse en `Shared/Infrastructure/Hooks/`.
|
||||||
|
|
||||||
|
### Archivo: CacheFirstHooksRegistrar.php
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Hooks;
|
||||||
|
|
||||||
|
final class CacheFirstHooksRegistrar
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
add_action('template_redirect', [$this, 'fireBeforePageServe'], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fireBeforePageServe(): void
|
||||||
|
{
|
||||||
|
if (is_user_logged_in()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_singular()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defined('REST_REQUEST') && REST_REQUEST) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$post_id = get_queried_object_id();
|
||||||
|
|
||||||
|
if ($post_id > 0) {
|
||||||
|
do_action('roi_theme_before_page_serve', $post_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registro en functions.php
|
||||||
|
|
||||||
|
```php
|
||||||
|
$cacheFirstHooks = new \ROITheme\Shared\Infrastructure\Hooks\CacheFirstHooksRegistrar();
|
||||||
|
$cacheFirstHooks->register();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. Hook `roi_theme_before_page_serve` se dispara en `template_redirect` priority 0
|
||||||
|
2. Solo dispara para `is_singular() === true`
|
||||||
|
3. NO dispara para usuarios logueados
|
||||||
|
4. Pasa `$post_id` como parametro
|
||||||
|
5. No define DONOTCACHEPAGE ni headers anti-cache
|
||||||
|
6. Plugins pueden enganchar y hacer redirect/exit
|
||||||
|
7. Sin impacto en rendimiento si ningun plugin engancha
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
| Version | Date | Changes |
|
||||||
|
|---------|------|---------|
|
||||||
|
| 1.0 | 2025-12-07 | Initial spec |
|
||||||
|
| 2.0 | 2025-12-07 | Simplified: Only defines hook, removed plugin implementation details |
|
||||||
Reference in New Issue
Block a user