refactor: reorganizar openspec y planificacion con spec recaptcha
- renombrar openspec/ a _openspec/ (carpeta auxiliar) - mover specs de features a changes/ - crear specs base: arquitectura-limpia, estandares-codigo, nomenclatura - migrar _planificacion/ con design-system y roi-theme-template - agregar especificacion recaptcha anti-spam (proposal, tasks, spec) - corregir rutas y referencias en todas las specs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
456
_openspec/AGENTS.md
Normal file
456
_openspec/AGENTS.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# OpenSpec Instructions
|
||||
|
||||
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||
|
||||
## TL;DR Quick Checklist
|
||||
|
||||
- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)
|
||||
- Decide scope: new capability vs modify existing capability
|
||||
- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)
|
||||
- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability
|
||||
- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement
|
||||
- Validate: `openspec validate [change-id] --strict` and fix issues
|
||||
- Request approval: Do not start implementation until proposal is approved
|
||||
|
||||
## Three-Stage Workflow
|
||||
|
||||
### Stage 1: Creating Changes
|
||||
Create proposal when you need to:
|
||||
- Add features or functionality
|
||||
- Make breaking changes (API, schema)
|
||||
- Change architecture or patterns
|
||||
- Optimize performance (changes behavior)
|
||||
- Update security patterns
|
||||
|
||||
Triggers (examples):
|
||||
- "Help me create a change proposal"
|
||||
- "Help me plan a change"
|
||||
- "Help me create a proposal"
|
||||
- "I want to create a spec proposal"
|
||||
- "I want to create a spec"
|
||||
|
||||
Loose matching guidance:
|
||||
- Contains one of: `proposal`, `change`, `spec`
|
||||
- With one of: `create`, `plan`, `make`, `start`, `help`
|
||||
|
||||
Skip proposal for:
|
||||
- Bug fixes (restore intended behavior)
|
||||
- Typos, formatting, comments
|
||||
- Dependency updates (non-breaking)
|
||||
- Configuration changes
|
||||
- Tests for existing behavior
|
||||
|
||||
**Workflow**
|
||||
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
|
||||
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
|
||||
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
|
||||
|
||||
### Stage 2: Implementing Changes
|
||||
Track these steps as TODOs and complete them one by one.
|
||||
1. **Read proposal.md** - Understand what's being built
|
||||
2. **Read design.md** (if exists) - Review technical decisions
|
||||
3. **Read tasks.md** - Get implementation checklist
|
||||
4. **Implement tasks sequentially** - Complete in order
|
||||
5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses
|
||||
6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality
|
||||
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||
|
||||
### Stage 3: Archiving Changes
|
||||
After deployment, create separate PR to:
|
||||
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
|
||||
- Update `specs/` if capabilities changed
|
||||
- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)
|
||||
- Run `openspec validate --strict` to confirm the archived change passes checks
|
||||
|
||||
## Before Any Task
|
||||
|
||||
**Context Checklist:**
|
||||
- [ ] Read relevant specs in `specs/[capability]/spec.md`
|
||||
- [ ] Check pending changes in `changes/` for conflicts
|
||||
- [ ] Read `openspec/project.md` for conventions
|
||||
- [ ] Run `openspec list` to see active changes
|
||||
- [ ] Run `openspec list --specs` to see existing capabilities
|
||||
|
||||
**Before Creating Specs:**
|
||||
- Always check if capability already exists
|
||||
- Prefer modifying existing specs over creating duplicates
|
||||
- Use `openspec show [spec]` to review current state
|
||||
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
|
||||
|
||||
### Search Guidance
|
||||
- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)
|
||||
- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)
|
||||
- Show details:
|
||||
- Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)
|
||||
- Change: `openspec show <change-id> --json --deltas-only`
|
||||
- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs`
|
||||
|
||||
## Quick Start
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```bash
|
||||
# Essential commands
|
||||
openspec list # List active changes
|
||||
openspec list --specs # List specifications
|
||||
openspec show [item] # Display change or spec
|
||||
openspec validate [item] # Validate changes or specs
|
||||
openspec archive <change-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
|
||||
|
||||
# Project management
|
||||
openspec init [path] # Initialize OpenSpec
|
||||
openspec update [path] # Update instruction files
|
||||
|
||||
# Interactive mode
|
||||
openspec show # Prompts for selection
|
||||
openspec validate # Bulk validation mode
|
||||
|
||||
# Debugging
|
||||
openspec show [change] --json --deltas-only
|
||||
openspec validate [change] --strict
|
||||
```
|
||||
|
||||
### Command Flags
|
||||
|
||||
- `--json` - Machine-readable output
|
||||
- `--type change|spec` - Disambiguate items
|
||||
- `--strict` - Comprehensive validation
|
||||
- `--no-interactive` - Disable prompts
|
||||
- `--skip-specs` - Archive without spec updates
|
||||
- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project conventions
|
||||
├── specs/ # Current truth - what IS built
|
||||
│ └── [capability]/ # Single focused capability
|
||||
│ ├── spec.md # Requirements and scenarios
|
||||
│ └── design.md # Technical patterns
|
||||
├── changes/ # Proposals - what SHOULD change
|
||||
│ ├── [change-name]/
|
||||
│ │ ├── proposal.md # Why, what, impact
|
||||
│ │ ├── tasks.md # Implementation checklist
|
||||
│ │ ├── design.md # Technical decisions (optional; see criteria)
|
||||
│ │ └── specs/ # Delta changes
|
||||
│ │ └── [capability]/
|
||||
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
|
||||
│ └── archive/ # Completed changes
|
||||
```
|
||||
|
||||
## Creating Change Proposals
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
New request?
|
||||
├─ Bug fix restoring spec behavior? → Fix directly
|
||||
├─ Typo/format/comment? → Fix directly
|
||||
├─ New feature/capability? → Create proposal
|
||||
├─ Breaking change? → Create proposal
|
||||
├─ Architecture change? → Create proposal
|
||||
└─ Unclear? → Create proposal (safer)
|
||||
```
|
||||
|
||||
### Proposal Structure
|
||||
|
||||
1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)
|
||||
|
||||
2. **Write proposal.md:**
|
||||
```markdown
|
||||
# Change: [Brief description of change]
|
||||
|
||||
## Why
|
||||
[1-2 sentences on problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
- [Bullet list of changes]
|
||||
- [Mark breaking changes with **BREAKING**]
|
||||
|
||||
## Impact
|
||||
- Affected specs: [list capabilities]
|
||||
- Affected code: [key files/systems]
|
||||
```
|
||||
|
||||
3. **Create spec deltas:** `specs/[capability]/spec.md`
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: New Feature
|
||||
The system SHALL provide...
|
||||
|
||||
#### Scenario: Success case
|
||||
- **WHEN** user performs action
|
||||
- **THEN** expected result
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Existing Feature
|
||||
[Complete modified requirement]
|
||||
|
||||
## REMOVED Requirements
|
||||
### Requirement: Old Feature
|
||||
**Reason**: [Why removing]
|
||||
**Migration**: [How to handle]
|
||||
```
|
||||
If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.
|
||||
|
||||
4. **Create tasks.md:**
|
||||
```markdown
|
||||
## 1. Implementation
|
||||
- [ ] 1.1 Create database schema
|
||||
- [ ] 1.2 Implement API endpoint
|
||||
- [ ] 1.3 Add frontend component
|
||||
- [ ] 1.4 Write tests
|
||||
```
|
||||
|
||||
5. **Create design.md when needed:**
|
||||
Create `design.md` if any of the following apply; otherwise omit it:
|
||||
- Cross-cutting change (multiple services/modules) or a new architectural pattern
|
||||
- New external dependency or significant data model changes
|
||||
- Security, performance, or migration complexity
|
||||
- Ambiguity that benefits from technical decisions before coding
|
||||
|
||||
Minimal `design.md` skeleton:
|
||||
```markdown
|
||||
## Context
|
||||
[Background, constraints, stakeholders]
|
||||
|
||||
## Goals / Non-Goals
|
||||
- Goals: [...]
|
||||
- Non-Goals: [...]
|
||||
|
||||
## Decisions
|
||||
- Decision: [What and why]
|
||||
- Alternatives considered: [Options + rationale]
|
||||
|
||||
## Risks / Trade-offs
|
||||
- [Risk] → Mitigation
|
||||
|
||||
## Migration Plan
|
||||
[Steps, rollback]
|
||||
|
||||
## Open Questions
|
||||
- [...]
|
||||
```
|
||||
|
||||
## Spec File Format
|
||||
|
||||
### Critical: Scenario Formatting
|
||||
|
||||
**CORRECT** (use #### headers):
|
||||
```markdown
|
||||
#### Scenario: User login success
|
||||
- **WHEN** valid credentials provided
|
||||
- **THEN** return JWT token
|
||||
```
|
||||
|
||||
**WRONG** (don't use bullets or bold):
|
||||
```markdown
|
||||
- **Scenario: User login** ❌
|
||||
**Scenario**: User login ❌
|
||||
### Scenario: User login ❌
|
||||
```
|
||||
|
||||
Every requirement MUST have at least one scenario.
|
||||
|
||||
### Requirement Wording
|
||||
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
|
||||
|
||||
### Delta Operations
|
||||
|
||||
- `## ADDED Requirements` - New capabilities
|
||||
- `## MODIFIED Requirements` - Changed behavior
|
||||
- `## REMOVED Requirements` - Deprecated features
|
||||
- `## RENAMED Requirements` - Name changes
|
||||
|
||||
Headers matched with `trim(header)` - whitespace ignored.
|
||||
|
||||
#### When to use ADDED vs MODIFIED
|
||||
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement.
|
||||
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.
|
||||
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.
|
||||
|
||||
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
|
||||
|
||||
Authoring a MODIFIED requirement correctly:
|
||||
1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.
|
||||
2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).
|
||||
3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.
|
||||
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.
|
||||
|
||||
Example for RENAMED:
|
||||
```markdown
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Login`
|
||||
- TO: `### Requirement: User Authentication`
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
**"Change must have at least one delta"**
|
||||
- Check `changes/[name]/specs/` exists with .md files
|
||||
- Verify files have operation prefixes (## ADDED Requirements)
|
||||
|
||||
**"Requirement must have at least one scenario"**
|
||||
- Check scenarios use `#### Scenario:` format (4 hashtags)
|
||||
- Don't use bullet points or bold for scenario headers
|
||||
|
||||
**Silent scenario parsing failures**
|
||||
- Exact format required: `#### Scenario: Name`
|
||||
- Debug with: `openspec show [change] --json --deltas-only`
|
||||
|
||||
### Validation Tips
|
||||
|
||||
```bash
|
||||
# Always use strict mode for comprehensive checks
|
||||
openspec validate [change] --strict
|
||||
|
||||
# Debug delta parsing
|
||||
openspec show [change] --json | jq '.deltas'
|
||||
|
||||
# Check specific requirement
|
||||
openspec show [spec] --json -r 1
|
||||
```
|
||||
|
||||
## Happy Path Script
|
||||
|
||||
```bash
|
||||
# 1) Explore current state
|
||||
openspec spec list --long
|
||||
openspec list
|
||||
# Optional full-text search:
|
||||
# rg -n "Requirement:|Scenario:" openspec/specs
|
||||
# rg -n "^#|Requirement:" openspec/changes
|
||||
|
||||
# 2) Choose change id and scaffold
|
||||
CHANGE=add-two-factor-auth
|
||||
mkdir -p openspec/changes/$CHANGE/{specs/auth}
|
||||
printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md
|
||||
printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md
|
||||
|
||||
# 3) Add deltas (example)
|
||||
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
Users MUST provide a second factor during login.
|
||||
|
||||
#### Scenario: OTP required
|
||||
- **WHEN** valid credentials are provided
|
||||
- **THEN** an OTP challenge is required
|
||||
EOF
|
||||
|
||||
# 4) Validate
|
||||
openspec validate $CHANGE --strict
|
||||
```
|
||||
|
||||
## Multi-Capability Example
|
||||
|
||||
```
|
||||
openspec/changes/add-2fa-notify/
|
||||
├── proposal.md
|
||||
├── tasks.md
|
||||
└── specs/
|
||||
├── auth/
|
||||
│ └── spec.md # ADDED: Two-Factor Authentication
|
||||
└── notifications/
|
||||
└── spec.md # ADDED: OTP email notification
|
||||
```
|
||||
|
||||
auth/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
...
|
||||
```
|
||||
|
||||
notifications/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: OTP Email Notification
|
||||
...
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Simplicity First
|
||||
- Default to <100 lines of new code
|
||||
- Single-file implementations until proven insufficient
|
||||
- Avoid frameworks without clear justification
|
||||
- Choose boring, proven patterns
|
||||
|
||||
### Complexity Triggers
|
||||
Only add complexity with:
|
||||
- Performance data showing current solution too slow
|
||||
- Concrete scale requirements (>1000 users, >100MB data)
|
||||
- Multiple proven use cases requiring abstraction
|
||||
|
||||
### Clear References
|
||||
- Use `file.ts:42` format for code locations
|
||||
- Reference specs as `specs/auth/spec.md`
|
||||
- Link related changes and PRs
|
||||
|
||||
### Capability Naming
|
||||
- Use verb-noun: `user-auth`, `payment-capture`
|
||||
- Single purpose per capability
|
||||
- 10-minute understandability rule
|
||||
- Split if description needs "AND"
|
||||
|
||||
### Change ID Naming
|
||||
- Use kebab-case, short and descriptive: `add-two-factor-auth`
|
||||
- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`
|
||||
- Ensure uniqueness; if taken, append `-2`, `-3`, etc.
|
||||
|
||||
## Tool Selection Guide
|
||||
|
||||
| Task | Tool | Why |
|
||||
|------|------|-----|
|
||||
| Find files by pattern | Glob | Fast pattern matching |
|
||||
| Search code content | Grep | Optimized regex search |
|
||||
| Read specific files | Read | Direct file access |
|
||||
| Explore unknown scope | Task | Multi-step investigation |
|
||||
|
||||
## Error Recovery
|
||||
|
||||
### Change Conflicts
|
||||
1. Run `openspec list` to see active changes
|
||||
2. Check for overlapping specs
|
||||
3. Coordinate with change owners
|
||||
4. Consider combining proposals
|
||||
|
||||
### Validation Failures
|
||||
1. Run with `--strict` flag
|
||||
2. Check JSON output for details
|
||||
3. Verify spec file format
|
||||
4. Ensure scenarios properly formatted
|
||||
|
||||
### Missing Context
|
||||
1. Read project.md first
|
||||
2. Check related specs
|
||||
3. Review recent archives
|
||||
4. Ask for clarification
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Stage Indicators
|
||||
- `changes/` - Proposed, not yet built
|
||||
- `specs/` - Built and deployed
|
||||
- `archive/` - Completed changes
|
||||
|
||||
### File Purposes
|
||||
- `proposal.md` - Why and what
|
||||
- `tasks.md` - Implementation steps
|
||||
- `design.md` - Technical decisions
|
||||
- `spec.md` - Requirements and behavior
|
||||
|
||||
### CLI Essentials
|
||||
```bash
|
||||
openspec list # What's in progress?
|
||||
openspec show [item] # View details
|
||||
openspec validate --strict # Is it correct?
|
||||
openspec archive <change-id> [--yes|-y] # Mark complete (add --yes for automation)
|
||||
```
|
||||
|
||||
Remember: Specs are truth. Changes are proposals. Keep them in sync.
|
||||
437
_openspec/WORKFLOW-ROI-THEME.md
Normal file
437
_openspec/WORKFLOW-ROI-THEME.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# WORKFLOW DE DESARROLLO ROI THEME
|
||||
|
||||
**ESTE ARCHIVO ES OBLIGATORIO LEER ANTES DE CUALQUIER DESARROLLO**
|
||||
|
||||
---
|
||||
|
||||
## REGLA DE ORO
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ SI NO EXISTE spec.md APROBADO → NO SE TOCA CÓDIGO │
|
||||
│ │
|
||||
│ NUNCA desarrollar directamente. SIEMPRE seguir el flujo de fases. │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RUTAS DEL PROYECTO
|
||||
|
||||
| Carpeta | Contenido | Ruta |
|
||||
|---------|-----------|------|
|
||||
| **Specs Base** | Arquitectura, estándares, nomenclatura | `_openspec/specs/` |
|
||||
| **Specs de Cambios** | Especificaciones por funcionalidad | `_openspec/changes/` |
|
||||
| **Schemas JSON** | Definición de componentes | `Schemas/` |
|
||||
| **Renderers** | Componentes frontend | `Public/[Componente]/` |
|
||||
| **FormBuilders** | Componentes admin | `Admin/[Componente]/` |
|
||||
| **Código Compartido** | Servicios, contratos, entidades | `Shared/` |
|
||||
|
||||
---
|
||||
|
||||
## CHECKLIST OBLIGATORIO (antes de cualquier desarrollo)
|
||||
|
||||
```
|
||||
[ ] 1. Leer este archivo (WORKFLOW-ROI-THEME.md)
|
||||
[ ] 2. Leer _openspec/specs/arquitectura-limpia.md
|
||||
[ ] 3. Leer _openspec/specs/estandares-codigo.md
|
||||
[ ] 4. Leer _openspec/specs/nomenclatura.md
|
||||
[ ] 5. Identificar qué se va a desarrollar (componente, servicio, fix)
|
||||
[ ] 6. Determinar el flujo apropiado:
|
||||
- Componente UI → Flujo de 5 Fases
|
||||
- Servicio/Feature → Flujo de 3 Fases
|
||||
- Bug fix simple → Flujo directo con spec mínima
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FLUJO DE 5 FASES (COMPONENTES UI)
|
||||
|
||||
> **Usar para:** Nuevos componentes visuales (Renderers + FormBuilders)
|
||||
|
||||
```
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ FASE 1: SCHEMA JSON ║
|
||||
║ "Definir la estructura de datos" ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Entrada: Template HTML de referencia (_planificacion/roi-theme-template/) ║
|
||||
║ Salida: Schemas/[nombre-en-kebab-case].json ║
|
||||
║ ║
|
||||
║ Contenido obligatorio: ║
|
||||
║ • component_name: kebab-case (ej: "featured-image") ║
|
||||
║ • version: "1.0.0" ║
|
||||
║ • description: Qué hace el componente ║
|
||||
║ • groups.visibility con 3 campos: ║
|
||||
║ - is_enabled (boolean, required) ║
|
||||
║ - show_on_desktop (boolean) ║
|
||||
║ - show_on_mobile (boolean) ║
|
||||
║ • Grupos adicionales con priority 10-90 ║
|
||||
║ ║
|
||||
║ Agente: roi-schema-architect ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ FASE 2: SINCRONIZACIÓN BD ║
|
||||
║ "Registrar configuración en base de datos" ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Entrada: Schema JSON creado en Fase 1 ║
|
||||
║ Salida: Registros en wp_roi_theme_component_settings ║
|
||||
║ ║
|
||||
║ Comando: ║
|
||||
║ powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' \ ║
|
||||
║ roi-theme sync-component [nombre-en-kebab-case]" ║
|
||||
║ ║
|
||||
║ Verificar: ║
|
||||
║ • Todos los campos del JSON existen en BD ║
|
||||
║ • Valores default aplicados correctamente ║
|
||||
║ • No hay campos huérfanos ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ FASE 3: RENDERER (Frontend) ║
|
||||
║ "Generar HTML y CSS desde BD" ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Entrada: Schema + Datos en BD ║
|
||||
║ Salida: Public/[PascalCase]/Infrastructure/Ui/[PascalCase]Renderer.php ║
|
||||
║ ║
|
||||
║ Requisitos obligatorios: ║
|
||||
║ • declare(strict_types=1) ║
|
||||
║ • Namespace: ROITheme\Public\[Component]\Infrastructure\Ui ║
|
||||
║ • final class [Component]Renderer implements RendererInterface ║
|
||||
║ • DI via constructor: CSSGeneratorInterface ║
|
||||
║ • COMPONENT_NAME constante en kebab-case ║
|
||||
║ • supports() retorna kebab-case ║
|
||||
║ • Validar: isEnabled(), getVisibilityClass() ║
|
||||
║ • CERO CSS hardcodeado (usar $this->cssGenerator) ║
|
||||
║ ║
|
||||
║ Agente: roi-renderer-builder ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ FASE 4: FORMBUILDER (Admin) ║
|
||||
║ "Panel de configuración en admin" ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Entrada: Schema + Renderer funcionando ║
|
||||
║ Salida: Admin/[PascalCase]/Infrastructure/Ui/[PascalCase]FormBuilder.php ║
|
||||
║ ║
|
||||
║ Requisitos obligatorios: ║
|
||||
║ • declare(strict_types=1) ║
|
||||
║ • Namespace: ROITheme\Admin\[Component]\Infrastructure\Ui ║
|
||||
║ • final class [Component]FormBuilder ║
|
||||
║ • DI via constructor: AdminDashboardRenderer ║
|
||||
║ • Design System: gradiente #0E2337 → #1e3a5f, borde #FF8600 ║
|
||||
║ • Bootstrap 5 form controls ║
|
||||
║ • data-component en kebab-case ║
|
||||
║ • Registrar en getComponents() con ID kebab-case ║
|
||||
║ ║
|
||||
║ Agente: roi-form-builder ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ FASE 5: VALIDACIÓN ║
|
||||
║ "Verificar que todo cumple las specs" ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Entrada: Componente completo (Schema + BD + Renderer + FormBuilder) ║
|
||||
║ Salida: Reporte de validación ║
|
||||
║ ║
|
||||
║ Comando: ║
|
||||
║ php Shared/Infrastructure/Scripts/validate-architecture.php [nombre] ║
|
||||
║ ║
|
||||
║ Verifica: ║
|
||||
║ • Estructura de carpetas correcta ║
|
||||
║ • JSON válido con campos obligatorios ║
|
||||
║ • Datos en BD sincronizados ║
|
||||
║ • Renderer implementa RendererInterface ║
|
||||
║ • FormBuilder registrado en getComponents() ║
|
||||
║ • Nomenclatura correcta (PascalCase/kebab-case) ║
|
||||
║ • Clean Architecture respetada (DI, capas) ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FLUJO DE 3 FASES (SERVICIOS/FEATURES)
|
||||
|
||||
> **Usar para:** Nuevos servicios, features no visuales, mejoras de infraestructura
|
||||
|
||||
```
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ FASE 1: ESPECIFICACIÓN ║
|
||||
║ "Definir qué se va a construir" ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Crear carpeta: _openspec/changes/[nombre-feature]/ ║
|
||||
║ ║
|
||||
║ Archivos a crear: ║
|
||||
║ • proposal.md - Por qué y qué se va a cambiar ║
|
||||
║ • tasks.md - Checklist de implementación ║
|
||||
║ • spec.md - Especificación detallada (formato Gherkin) ║
|
||||
║ ║
|
||||
║ >>> REQUIERE APROBACIÓN DEL USUARIO PARA CONTINUAR <<< ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ FASE 2: IMPLEMENTACIÓN ║
|
||||
║ "Construir siguiendo la spec" ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Ubicación según tipo: ║
|
||||
║ • Servicios compartidos → Shared/Infrastructure/Services/ ║
|
||||
║ • Contratos → Shared/Domain/Contracts/ ║
|
||||
║ • UseCases → [Context]/Application/UseCases/ ║
|
||||
║ • Repositorios → [Context]/Infrastructure/Persistence/WordPress/ ║
|
||||
║ ║
|
||||
║ Requisitos: ║
|
||||
║ • declare(strict_types=1) ║
|
||||
║ • Namespace correcto según ubicación ║
|
||||
║ • DI via constructor (interfaces, no clases concretas) ║
|
||||
║ • Métodos pequeños (<30 líneas) ║
|
||||
║ • Clases pequeñas (<300 líneas) ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ FASE 3: INTEGRACIÓN Y PRUEBA ║
|
||||
║ "Conectar y verificar" ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ • Integrar servicio donde se necesite ║
|
||||
║ • Probar funcionalidad en navegador ║
|
||||
║ • Verificar que no hay errores PHP ║
|
||||
║ • Actualizar tasks.md con estado COMPLETADO ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CÓMO CREAR NUEVA FEATURE
|
||||
|
||||
### Paso 1: Crear estructura de carpetas
|
||||
|
||||
```
|
||||
_openspec/changes/[nombre-feature]/
|
||||
├── proposal.md ← Por qué y qué se va a cambiar
|
||||
├── tasks.md ← Checklist → evoluciona a tracking
|
||||
└── spec.md ← Especificación detallada
|
||||
```
|
||||
|
||||
### Paso 2: Crear proposal.md
|
||||
|
||||
```markdown
|
||||
# Proposal: [Nombre Feature]
|
||||
|
||||
## Why
|
||||
[1-2 oraciones sobre el problema a resolver]
|
||||
|
||||
## What Changes
|
||||
- [Lista de cambios propuestos]
|
||||
|
||||
## Impact
|
||||
- Archivos nuevos: [lista]
|
||||
- Archivos modificados: [lista]
|
||||
- Specs relacionadas: [lista]
|
||||
```
|
||||
|
||||
### Paso 3: Crear tasks.md
|
||||
|
||||
```markdown
|
||||
# Tracking: [nombre-feature]
|
||||
|
||||
**Estado actual:** EN PROGRESO
|
||||
**Próximo paso:** [describir]
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
### Fase 1: Especificación
|
||||
- [ ] Crear proposal.md
|
||||
- [ ] Crear spec.md con Requirements y Scenarios
|
||||
- [ ] Aprobar spec.md
|
||||
|
||||
### Fase 2: Implementación
|
||||
- [ ] Crear interface (si aplica)
|
||||
- [ ] Crear implementación
|
||||
- [ ] Registrar en DI container (si aplica)
|
||||
|
||||
### Fase 3: Integración
|
||||
- [ ] Integrar donde se necesite
|
||||
- [ ] Probar funcionalidad
|
||||
- [ ] Verificar sin errores
|
||||
|
||||
---
|
||||
|
||||
## Historial
|
||||
| Fecha | Avance |
|
||||
|-------|--------|
|
||||
| YYYY-MM-DD | [descripción] |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NOMENCLATURA RÁPIDA
|
||||
|
||||
| Contexto | Formato | Ejemplo |
|
||||
|----------|---------|---------|
|
||||
| component_name (JSON/BD) | kebab-case | `"featured-image"` |
|
||||
| Archivo schema | kebab-case | `featured-image.json` |
|
||||
| Carpeta módulo | PascalCase | `FeaturedImage/` |
|
||||
| Namespace PHP | PascalCase | `ROITheme\Public\FeaturedImage\...` |
|
||||
| Clase Renderer | PascalCase | `FeaturedImageRenderer` |
|
||||
| Clase FormBuilder | PascalCase | `FeaturedImageFormBuilder` |
|
||||
| Constante | UPPER_SNAKE | `COMPONENT_NAME` |
|
||||
| Método | camelCase | `getVisibilityClass()` |
|
||||
| Variable | $camelCase | `$showDesktop` |
|
||||
|
||||
**Conversión kebab ↔ Pascal:** `featured-image` ↔ `FeaturedImage`
|
||||
|
||||
---
|
||||
|
||||
## CLEAN ARCHITECTURE RESUMIDA
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ DOMAIN │
|
||||
│ (Centro, sin deps) │
|
||||
│ │
|
||||
│ • Entities (Component, etc.) │
|
||||
│ • Contracts/Interfaces │
|
||||
│ • Value Objects │
|
||||
│ │
|
||||
│ PROHIBIDO: WordPress, echo, print, HTML │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ APPLICATION │
|
||||
│ (Usa Domain) │
|
||||
│ │
|
||||
│ • UseCases │
|
||||
│ • Application Services │
|
||||
│ │
|
||||
│ PROHIBIDO: WordPress, acceso BD directo │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ INFRASTRUCTURE │
|
||||
│ (Usa Domain y Application) │
|
||||
│ │
|
||||
│ • Ui/ (Renderers, FormBuilders) │
|
||||
│ • Api/ (AJAX handlers, REST) │
|
||||
│ • Persistence/ (Repositories WordPress) │
|
||||
│ • Services/ (Implementaciones) │
|
||||
│ │
|
||||
│ PERMITIDO: WordPress, HTML, BD, APIs externas │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Dirección de dependencias: Infrastructure → Application → Domain
|
||||
(afuera depende de adentro, NUNCA al revés)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## COMANDOS WP-CLI
|
||||
|
||||
```powershell
|
||||
# Ubicación WP-CLI
|
||||
C:\xampp\php_8.0.30_backup\wp-cli.phar
|
||||
|
||||
# Sincronizar un componente
|
||||
powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-component [nombre]"
|
||||
|
||||
# Sincronizar todos los componentes
|
||||
powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-all-components"
|
||||
|
||||
# Ejemplo
|
||||
powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-component featured-image"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AGENTES DISPONIBLES
|
||||
|
||||
| Agente | Propósito | Cuándo usar |
|
||||
|--------|-----------|-------------|
|
||||
| roi-schema-architect | Genera JSON schemas desde HTML | Fase 1 de componentes |
|
||||
| roi-renderer-builder | Genera Renderers PHP | Fase 3 de componentes |
|
||||
| roi-form-builder | Genera FormBuilders PHP | Fase 4 de componentes |
|
||||
|
||||
---
|
||||
|
||||
## ESPECIFICACIONES BASE (LECTURA OBLIGATORIA)
|
||||
|
||||
| Spec | Ruta | Contenido |
|
||||
|------|------|-----------|
|
||||
| Arquitectura | `_openspec/specs/arquitectura-limpia.md` | Capas, dependencias, estructura |
|
||||
| Estándares | `_openspec/specs/estandares-codigo.md` | SOLID, límites, WordPress |
|
||||
| Nomenclatura | `_openspec/specs/nomenclatura.md` | Nombres, formatos, convenciones |
|
||||
|
||||
---
|
||||
|
||||
## ROLES
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ CLAUDE CODE (Yo) USUARIO (Tú) │
|
||||
│ ───────────────── ──────────── │
|
||||
│ │
|
||||
│ • Ejecuto los agentes • Defines qué componente crear │
|
||||
│ • Genero schemas JSON • Apruebas especificaciones │
|
||||
│ • Genero Renderers • Apruebas diseño │
|
||||
│ • Genero FormBuilders • Pruebas en navegador │
|
||||
│ • Ejecuto validaciones • Verificas funcionamiento │
|
||||
│ • Documento en specs • Decides prioridades │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GARANTÍAS DEL PROCESO
|
||||
|
||||
1. **NUNCA** se escribe código sin spec aprobada
|
||||
2. **SIEMPRE** se sigue el flujo de fases
|
||||
3. **CADA** componente tiene las 5 partes (Schema, BD, Renderer, FormBuilder, Validación)
|
||||
4. **TODO** el CSS se genera vía CSSGenerator (cero hardcodeado)
|
||||
5. **SIEMPRE** DI via constructor (interfaces, no clases concretas)
|
||||
6. **NUNCA** WordPress en Domain ni Application
|
||||
|
||||
---
|
||||
|
||||
## LECCIONES APRENDIDAS
|
||||
|
||||
> Esta sección documenta ajustes al workflow basados en implementaciones completadas.
|
||||
|
||||
### Validación código vs specs (2026-01-08)
|
||||
|
||||
**Contexto:** Antes de mejorar specs, se validó que el código actual cumple con las especificaciones propuestas.
|
||||
|
||||
**Hallazgos:**
|
||||
- 17 Renderers siguen el patrón correctamente
|
||||
- 17 FormBuilders siguen el patrón correctamente
|
||||
- 17 Schemas en kebab-case
|
||||
- 43 archivos con strict_types=1
|
||||
- 39 clases final
|
||||
- 23 interfaces en Domain/Contracts
|
||||
|
||||
**Conclusión:** El código YA implementa Clean Architecture. Las mejoras a specs son documentación de patrones existentes, no nuevas invenciones.
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2026-01-08
|
||||
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`
|
||||
365
_openspec/changes/adsense-auto-ads-toggle/spec.md
Normal file
365
_openspec/changes/adsense-auto-ads-toggle/spec.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# Especificacion: AdSense Auto Ads Toggle
|
||||
|
||||
## Purpose
|
||||
|
||||
Define el comportamiento de un interruptor "Auto Ads" que cuando se activa, delega el control de ubicacion de anuncios a Google AdSense Auto Ads, desactivando automaticamente todas las ubicaciones manuales del tema EXCEPTO los anuncios en resultados de busqueda del plugin ROI APU Search.
|
||||
|
||||
**Problema que resuelve:** El usuario necesita probar Google Auto Ads sin tener que deshabilitar manualmente cada ubicacion de anuncio. Ademas, Auto Ads tiende a romper el layout de tablas HTML, por lo que se requiere excluirlas automaticamente.
|
||||
|
||||
**Beneficio esperado:** Comparar facilmente el rendimiento (RPM, CTR, revenue) entre ubicaciones manuales vs Auto Ads de Google, manteniendo siempre activos los anuncios en el buscador APU.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Campo enable_auto_ads en Schema
|
||||
|
||||
El schema adsense-placement.json DEBE incluir un nuevo campo para activar Auto Ads.
|
||||
|
||||
#### Scenario: Definicion del campo en JSON Schema
|
||||
- **WHEN** se define el campo `enable_auto_ads`
|
||||
- **THEN** DEBE ubicarse en el grupo `visibility` con priority 10
|
||||
- **AND** DEBE tener type `boolean`
|
||||
- **AND** DEBE tener default `false`
|
||||
- **AND** DEBE tener label `Activar Google Auto Ads`
|
||||
- **AND** DEBE tener description `Cuando esta activo, Google controla automaticamente donde mostrar anuncios. Se desactivan las ubicaciones manuales excepto los anuncios en busqueda.`
|
||||
|
||||
#### Scenario: Posicion del campo en el formulario
|
||||
- **WHEN** se renderiza el formulario de configuracion
|
||||
- **THEN** el campo `enable_auto_ads` DEBE aparecer inmediatamente despues de `is_enabled`
|
||||
- **AND** DEBE tener un indicador visual destacado (badge o icono) indicando que es modo automatico
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Desactivacion Automatica de Ubicaciones Manuales
|
||||
|
||||
Cuando Auto Ads esta activo, el sistema DEBE ignorar todas las configuraciones de ubicacion manual.
|
||||
|
||||
#### Scenario: Grupos afectados por enable_auto_ads
|
||||
- **GIVEN** `enable_auto_ads === true`
|
||||
- **WHEN** se evaluan las ubicaciones de anuncios
|
||||
- **THEN** los siguientes grupos DEBEN comportarse como si estuvieran desactivados:
|
||||
- `incontent_advanced` (In-Content Ads Avanzado)
|
||||
- `behavior` (Ubicaciones en Posts) - EXCEPTO `javascript_first_mode`
|
||||
- `anchor_ads` (Anuncios Fijos)
|
||||
- `vignette_ads` (Anuncios de Vineta)
|
||||
- `layout` (Ubicaciones Archivos/Globales)
|
||||
- **AND** el grupo `search_results` DEBE permanecer activo
|
||||
- **AND** el grupo `analytics` DEBE permanecer activo
|
||||
- **AND** el grupo `content` (credenciales) DEBE permanecer activo
|
||||
|
||||
#### Scenario: Preservacion de configuracion en base de datos
|
||||
- **GIVEN** el usuario activa `enable_auto_ads`
|
||||
- **WHEN** se guardan los settings
|
||||
- **THEN** los valores de los campos desactivados NO DEBEN modificarse en la BD
|
||||
- **AND** cuando el usuario desactive `enable_auto_ads`, sus configuraciones previas DEBEN estar intactas
|
||||
|
||||
#### Scenario: Evaluacion en PHP (Server-Side)
|
||||
- **GIVEN** `enable_auto_ads === true` en los settings
|
||||
- **WHEN** un Renderer evalua si debe insertar un slot de anuncio
|
||||
- **THEN** DEBE verificar primero si `enable_auto_ads` esta activo
|
||||
- **AND** si esta activo, DEBE retornar inmediatamente sin insertar el slot manual
|
||||
- **AND** EXCEPTO para slots del grupo `search_results`
|
||||
|
||||
#### Scenario: Evaluacion en JavaScript (Client-Side)
|
||||
- **GIVEN** JavaScript-First Mode esta activo
|
||||
- **AND** `enable_auto_ads === true`
|
||||
- **WHEN** el controller JavaScript evalua visibilidad
|
||||
- **THEN** DEBE incluir `enable_auto_ads` en el response del endpoint REST
|
||||
- **AND** el script DEBE ocultar slots manuales marcados con `data-ad-manual`
|
||||
- **AND** NO DEBE ocultar slots marcados con `data-ad-search`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Excepcion para ROI APU Search
|
||||
|
||||
Los anuncios en resultados de busqueda DEBEN permanecer activos independientemente del estado de Auto Ads.
|
||||
|
||||
#### Scenario: Configuracion de busqueda siempre disponible
|
||||
- **GIVEN** `enable_auto_ads === true`
|
||||
- **WHEN** se obtiene la configuracion para el plugin roi-apu-search via `roi_get_adsense_search_config()`
|
||||
- **THEN** DEBE retornar la configuracion del grupo `search_results`
|
||||
- **AND** `search_ads_enabled` DEBE evaluarse independientemente de `enable_auto_ads`
|
||||
|
||||
#### Scenario: Slots de busqueda con atributo especial
|
||||
- **WHEN** se renderizan slots de anuncios para resultados de busqueda
|
||||
- **THEN** DEBEN incluir atributo `data-ad-search="true"`
|
||||
- **AND** DEBEN incluir clase CSS `roi-ad-search-slot`
|
||||
- **AND** NO DEBEN incluir atributo `data-ad-manual`
|
||||
|
||||
#### Scenario: JavaScript no oculta slots de busqueda
|
||||
- **GIVEN** el controller JavaScript detecta `enable_auto_ads === true`
|
||||
- **WHEN** procesa los slots de anuncios
|
||||
- **THEN** NO DEBE agregar clase `roi-ad-hidden` a elementos con `data-ad-search`
|
||||
- **AND** DEBE disparar evento `roi-adsense-activate` para slots de busqueda
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CSS para Excluir Tablas de Auto Ads
|
||||
|
||||
El sistema DEBE inyectar CSS que indica a Google Auto Ads que NO inserte anuncios dentro de tablas.
|
||||
|
||||
#### Scenario: Inyeccion de CSS google-auto-ads ignore
|
||||
- **GIVEN** `enable_auto_ads === true`
|
||||
- **WHEN** se renderiza el header del sitio
|
||||
- **THEN** DEBE inyectar el siguiente CSS:
|
||||
```css
|
||||
table,
|
||||
.wp-block-table,
|
||||
.tablepress,
|
||||
figure.wp-block-table,
|
||||
.entry-content table {
|
||||
google-auto-ads: ignore;
|
||||
}
|
||||
```
|
||||
- **AND** el CSS DEBE cargarse ANTES del script de AdSense
|
||||
|
||||
#### Scenario: CSS solo cuando Auto Ads esta activo
|
||||
- **GIVEN** `enable_auto_ads === false`
|
||||
- **WHEN** se renderiza el header
|
||||
- **THEN** NO DEBE inyectar el CSS de `google-auto-ads: ignore`
|
||||
|
||||
#### Scenario: Selectores adicionales configurables
|
||||
- **GIVEN** el usuario necesita excluir elementos adicionales
|
||||
- **WHEN** existe un campo `auto_ads_exclude_selectors` en el schema
|
||||
- **THEN** DEBE agregar esos selectores al CSS de exclusion
|
||||
- **AND** el campo DEBE ser de tipo `textarea`
|
||||
- **AND** DEBE tener default vacio
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Script de Google Auto Ads
|
||||
|
||||
Cuando Auto Ads esta activo, el sistema DEBE insertar el script requerido por Google.
|
||||
|
||||
#### Scenario: Insercion del script adsbygoogle
|
||||
- **GIVEN** `enable_auto_ads === true`
|
||||
- **AND** `is_enabled === true`
|
||||
- **WHEN** se renderiza el head del documento
|
||||
- **THEN** DEBE insertar el script de AdSense:
|
||||
```html
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXX"
|
||||
crossorigin="anonymous"></script>
|
||||
```
|
||||
- **AND** DEBE usar el `publisher_id` configurado en el grupo `content`
|
||||
|
||||
#### Scenario: No duplicar scripts
|
||||
- **GIVEN** el tema ya encola el script de AdSense para ubicaciones manuales
|
||||
- **WHEN** `enable_auto_ads === true`
|
||||
- **THEN** NO DEBE duplicar el script
|
||||
- **AND** DEBE reutilizar el script existente
|
||||
|
||||
#### Scenario: Delay de carga respetado
|
||||
- **GIVEN** `delay_enabled === true` en el grupo `forms`
|
||||
- **AND** `enable_auto_ads === true`
|
||||
- **WHEN** se carga la pagina
|
||||
- **THEN** el script de AdSense DEBE cargarse con delay
|
||||
- **AND** DEBE respetar el valor de `delay_timeout`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Indicador Visual en Admin UI
|
||||
|
||||
El panel de administracion DEBE mostrar claramente cuando Auto Ads esta activo.
|
||||
|
||||
#### Scenario: Badge de estado Auto Ads
|
||||
- **GIVEN** el usuario accede al panel de configuracion de AdSense
|
||||
- **WHEN** `enable_auto_ads === true`
|
||||
- **THEN** DEBE mostrar un badge visible "AUTO ADS ACTIVO" en color naranja (#FF8600)
|
||||
- **AND** los grupos desactivados DEBEN aparecer con opacidad reducida (0.5)
|
||||
- **AND** los grupos desactivados DEBEN mostrar tooltip "Desactivado por Auto Ads"
|
||||
|
||||
#### Scenario: Seccion de grupos no afectados
|
||||
- **GIVEN** `enable_auto_ads === true`
|
||||
- **WHEN** se renderiza el formulario
|
||||
- **THEN** los grupos `search_results`, `analytics`, y `content` DEBEN renderizarse normalmente
|
||||
- **AND** DEBEN tener indicador "Siempre activo" visible
|
||||
|
||||
#### Scenario: Confirmacion al activar
|
||||
- **GIVEN** el usuario hace click en `enable_auto_ads`
|
||||
- **WHEN** el campo cambia de false a true
|
||||
- **THEN** DEBE mostrar dialogo de confirmacion con mensaje:
|
||||
"Al activar Auto Ads, Google controlara automaticamente donde mostrar anuncios.
|
||||
Tus configuraciones manuales se mantendran guardadas pero no se usaran.
|
||||
Los anuncios en resultados de busqueda seguiran funcionando.
|
||||
Continuar?"
|
||||
- **AND** DEBE tener botones "Activar Auto Ads" y "Cancelar"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Separacion de Capas segun Clean Architecture
|
||||
|
||||
La implementacion DEBE seguir Clean Architecture.
|
||||
|
||||
#### Scenario: Value Object AutoAdsConfiguration en Domain
|
||||
- **WHEN** se crea el Value Object AutoAdsConfiguration
|
||||
- **THEN** DEBE ubicarse en `Shared/Domain/ValueObjects/`
|
||||
- **AND** DEBE ser inmutable despues de construccion
|
||||
- **AND** DEBE exponer `isAutoAdsEnabled()`, `getExcludedSelectors()`, `getPublisherId()`
|
||||
- **AND** NO DEBE contener logica de WordPress
|
||||
|
||||
#### Scenario: Interface en Domain
|
||||
- **WHEN** se define el contrato para verificar si un grupo esta activo
|
||||
- **THEN** DEBE existir metodo en `AdsenseSettingsInterface`:
|
||||
`isGroupActiveWithAutoAds(string $groupName): bool`
|
||||
- **AND** DEBE retornar false para grupos manuales cuando Auto Ads esta activo
|
||||
- **AND** DEBE retornar true para `search_results`, `analytics`, `content`
|
||||
|
||||
#### Scenario: Service en Infrastructure
|
||||
- **WHEN** se implementa la logica de evaluacion
|
||||
- **THEN** el `AdsenseSettingsService` DEBE verificar `enable_auto_ads` antes de evaluar ubicaciones
|
||||
- **AND** DEBE exponer metodo `getActiveGroups(): array` que retorna solo grupos activos
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Endpoint REST para Estado de Auto Ads
|
||||
|
||||
El endpoint de visibilidad DEBE informar el estado de Auto Ads.
|
||||
|
||||
#### Scenario: Inclusion en response de visibility
|
||||
- **GIVEN** el endpoint `/wp-json/roi-theme/v1/adsense-placement/visibility`
|
||||
- **WHEN** responde exitosamente
|
||||
- **THEN** DEBE incluir campo `auto_ads_enabled: boolean`
|
||||
- **AND** DEBE incluir campo `auto_ads_exclude_selectors: string`
|
||||
|
||||
#### Scenario: Response cuando Auto Ads esta activo
|
||||
- **GIVEN** `enable_auto_ads === true`
|
||||
- **WHEN** se llama al endpoint
|
||||
- **THEN** el response DEBE incluir:
|
||||
```json
|
||||
{
|
||||
"show_ads": true,
|
||||
"auto_ads_enabled": true,
|
||||
"auto_ads_exclude_selectors": "table,.wp-block-table",
|
||||
"manual_slots_disabled": true,
|
||||
"search_slots_enabled": true,
|
||||
"reasons": [],
|
||||
"cache_seconds": 300,
|
||||
"timestamp": 1733900000
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Migracion y Retrocompatibilidad
|
||||
|
||||
La implementacion DEBE mantener compatibilidad con instalaciones existentes.
|
||||
|
||||
#### Scenario: Valor default para instalaciones existentes
|
||||
- **GIVEN** una instalacion existente sin el campo `enable_auto_ads`
|
||||
- **WHEN** se ejecuta sync-component
|
||||
- **THEN** DEBE crear el campo con valor `false`
|
||||
- **AND** el comportamiento existente NO DEBE cambiar
|
||||
|
||||
#### Scenario: Sin cambios en logica si Auto Ads esta desactivado
|
||||
- **GIVEN** `enable_auto_ads === false`
|
||||
- **WHEN** se evaluan ubicaciones de anuncios
|
||||
- **THEN** el comportamiento DEBE ser identico al actual
|
||||
- **AND** NO DEBE haber impacto en rendimiento
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Logging y Diagnostico
|
||||
|
||||
El sistema DEBE proveer informacion de diagnostico para troubleshooting.
|
||||
|
||||
#### Scenario: Log cuando Auto Ads ignora ubicacion
|
||||
- **GIVEN** `enable_auto_ads === true`
|
||||
- **AND** WP_DEBUG === true
|
||||
- **WHEN** un Renderer es llamado para ubicacion manual
|
||||
- **THEN** DEBE loguear mensaje: "AdSense: Skipping manual slot '{slot_name}' - Auto Ads active"
|
||||
- **AND** el log DEBE ser level DEBUG
|
||||
|
||||
#### Scenario: Comentario HTML de diagnostico
|
||||
- **GIVEN** `enable_auto_ads === true`
|
||||
- **AND** WP_DEBUG === true
|
||||
- **WHEN** se renderiza el header
|
||||
- **THEN** DEBE incluir comentario HTML:
|
||||
```html
|
||||
<!-- ROI AdSense: Auto Ads Mode Active | Manual slots: disabled | Search slots: enabled -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Schema Changes (adsense-placement.json)
|
||||
|
||||
Agregar al grupo `visibility`:
|
||||
|
||||
```json
|
||||
"enable_auto_ads": {
|
||||
"type": "boolean",
|
||||
"label": "Activar Google Auto Ads",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "Google controla automaticamente la ubicacion de anuncios. Se desactivan ubicaciones manuales excepto busqueda."
|
||||
}
|
||||
```
|
||||
|
||||
Agregar al grupo `forms` (nuevo campo):
|
||||
|
||||
```json
|
||||
"auto_ads_exclude_selectors": {
|
||||
"type": "textarea",
|
||||
"label": "Selectores adicionales a excluir de Auto Ads",
|
||||
"default": "",
|
||||
"editable": true,
|
||||
"description": "Selectores CSS adicionales donde Auto Ads no debe insertar anuncios (uno por linea)"
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Default Exclusions
|
||||
|
||||
```css
|
||||
/* Elementos que Google Auto Ads debe ignorar */
|
||||
table,
|
||||
.wp-block-table,
|
||||
.tablepress,
|
||||
figure.wp-block-table,
|
||||
.entry-content table,
|
||||
.roi-apu-results,
|
||||
.roi-apu-result-item,
|
||||
pre,
|
||||
code,
|
||||
.wp-block-code,
|
||||
.syntax-highlighted {
|
||||
google-auto-ads: ignore;
|
||||
}
|
||||
```
|
||||
|
||||
### Grupos y su comportamiento con Auto Ads
|
||||
|
||||
| Grupo | Con Auto Ads OFF | Con Auto Ads ON |
|
||||
|-------|------------------|-----------------|
|
||||
| visibility | Activo | Activo (controla is_enabled global) |
|
||||
| analytics | Activo | Activo |
|
||||
| content | Activo | Activo (provee publisher_id) |
|
||||
| incontent_advanced | Activo | IGNORADO |
|
||||
| behavior | Activo | IGNORADO (excepto javascript_first_mode) |
|
||||
| anchor_ads | Activo | IGNORADO |
|
||||
| vignette_ads | Activo | IGNORADO |
|
||||
| search_results | Activo | ACTIVO (excepcion) |
|
||||
| layout | Activo | IGNORADO |
|
||||
| forms | Activo | Activo (delay, exclusiones) |
|
||||
|
||||
---
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Antes de considerar esta especificacion implementada:
|
||||
|
||||
- [ ] Campo `enable_auto_ads` existe en schema y BD
|
||||
- [ ] Campo `auto_ads_exclude_selectors` existe en schema y BD
|
||||
- [ ] CSS de exclusion se inyecta solo cuando Auto Ads activo
|
||||
- [ ] Slots manuales no se renderizan cuando Auto Ads activo
|
||||
- [ ] Slots de busqueda SI se renderizan cuando Auto Ads activo
|
||||
- [ ] `roi_get_adsense_search_config()` funciona independiente de Auto Ads
|
||||
- [ ] Admin UI muestra indicador visual de Auto Ads activo
|
||||
- [ ] Grupos desactivados aparecen con opacidad reducida
|
||||
- [ ] Endpoint REST incluye `auto_ads_enabled` en response
|
||||
- [ ] JavaScript oculta slots manuales pero no de busqueda
|
||||
- [ ] Script de AdSense se carga correctamente
|
||||
- [ ] No hay duplicacion de scripts
|
||||
- [ ] Logs de debug funcionan cuando WP_DEBUG activo
|
||||
- [ ] Sync-component migra correctamente instalaciones existentes
|
||||
309
_openspec/changes/adsense-cache-unified-visibility/spec.md
Normal file
309
_openspec/changes/adsense-cache-unified-visibility/spec.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# Especificacion: Unificacion de Visibilidad AdSense para Compatibilidad con Cache
|
||||
|
||||
## Purpose
|
||||
|
||||
Unificar la logica de visibilidad de AdSense para que TODA la evaluacion dependiente de usuario
|
||||
(hide_for_logged_in) se realice en el cliente via JavaScript-First, eliminando llamadas a
|
||||
`is_user_logged_in()` durante el render PHP.
|
||||
|
||||
**Problema actual:** El `AdsensePlacementRenderer` usa `PageVisibilityHelper::shouldShow()` que
|
||||
internamente llama `is_user_logged_in()` en PHP. Esto causa que el HTML generado varie segun el
|
||||
estado de autenticacion, rompiendo la compatibilidad con page cache.
|
||||
|
||||
**Solucion:** Cuando `javascript_first_mode` esta activo, el Renderer debe generar SIEMPRE el HTML
|
||||
de los slots, delegando la decision de `hide_for_logged_in` al endpoint REST y al JavaScript del cliente.
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
### Arquitectura JavaScript-First existente (spec: adsense-javascript-first)
|
||||
|
||||
El sistema JavaScript-First ya implementa:
|
||||
1. **Endpoint REST** `/wp-json/roi-theme/v1/adsense-placement/visibility`
|
||||
- Evalua `is_user_logged_in()` en tiempo real (no cacheado)
|
||||
- Retorna decision `show_ads: true/false` con razones
|
||||
- Headers anti-cache (no-store, no-cache)
|
||||
|
||||
2. **Cliente JavaScript** `adsense-visibility.js`
|
||||
- Consulta endpoint via AJAX con cookies (credentials: same-origin)
|
||||
- Cachea decision en localStorage
|
||||
- Aplica clases CSS para mostrar/ocultar slots
|
||||
|
||||
### El gap actual
|
||||
|
||||
```
|
||||
FLUJO ACTUAL (PROBLEMATICO):
|
||||
|
||||
Request → PHP Render → PageVisibilityHelper::shouldShow()
|
||||
↓
|
||||
is_user_logged_in() ← ROMPE CACHE
|
||||
↓
|
||||
HTML con/sin slots (depende de login)
|
||||
↓
|
||||
Page Cache ← HTML incorrecto para otros usuarios
|
||||
```
|
||||
|
||||
```
|
||||
FLUJO DESEADO (CON ESTA SPEC):
|
||||
|
||||
Request → PHP Render → [javascript_first_mode=true?]
|
||||
↓ SI
|
||||
SIEMPRE genera HTML con slots
|
||||
↓
|
||||
Page Cache ← HTML identico para todos
|
||||
↓
|
||||
JS consulta endpoint REST
|
||||
↓
|
||||
Muestra/oculta segun respuesta
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Bypass de hide_for_logged_in en modo JavaScript-First
|
||||
|
||||
The AdsensePlacementRenderer MUST skip the `is_user_logged_in()` evaluation during PHP render
|
||||
when `javascript_first_mode` is enabled.
|
||||
|
||||
#### Scenario: JavaScript-First activo, usuario anonimo
|
||||
- **GIVEN** `javascript_first_mode` esta habilitado en settings
|
||||
- **AND** `hide_for_logged_in` esta habilitado
|
||||
- **AND** el visitante NO esta logueado
|
||||
- **WHEN** se renderiza un slot de AdSense
|
||||
- **THEN** el HTML del slot DEBE generarse (placeholders visibles)
|
||||
- **AND** el JavaScript DEBE consultar el endpoint
|
||||
- **AND** el endpoint retorna `show_ads: true`
|
||||
- **AND** los anuncios se muestran
|
||||
|
||||
#### Scenario: JavaScript-First activo, usuario logueado
|
||||
- **GIVEN** `javascript_first_mode` esta habilitado
|
||||
- **AND** `hide_for_logged_in` esta habilitado
|
||||
- **AND** el visitante ESTA logueado
|
||||
- **WHEN** se renderiza un slot de AdSense
|
||||
- **THEN** el HTML del slot DEBE generarse (en cache seria identico a anonimo)
|
||||
- **AND** el JavaScript DEBE consultar el endpoint
|
||||
- **AND** el endpoint retorna `show_ads: false` con razon `hide_for_logged_in`
|
||||
- **AND** los anuncios se OCULTAN via CSS/JS
|
||||
|
||||
#### Scenario: JavaScript-First deshabilitado (modo legacy)
|
||||
- **GIVEN** `javascript_first_mode` esta DESHABILITADO
|
||||
- **AND** `hide_for_logged_in` esta habilitado
|
||||
- **AND** el visitante ESTA logueado
|
||||
- **WHEN** se renderiza un slot de AdSense
|
||||
- **THEN** el HTML del slot NO se genera (comportamiento legacy)
|
||||
- **AND** `is_user_logged_in()` SE evalua en PHP
|
||||
- **BECAUSE** sin JS-First, el modo legacy es la unica opcion
|
||||
|
||||
---
|
||||
|
||||
### Requirement 2: PageVisibilityHelper debe respetar modo JavaScript-First
|
||||
|
||||
The PageVisibilityHelper MUST provide a method that excludes user-dependent checks when
|
||||
JavaScript-First mode is active for a component.
|
||||
|
||||
#### Scenario: Nuevo metodo shouldShowForCache
|
||||
- **GIVEN** un componente con `javascript_first_mode` habilitado
|
||||
- **WHEN** se llama `PageVisibilityHelper::shouldShowForCache('adsense-placement')`
|
||||
- **THEN** DEBE evaluar visibilidad por tipo de pagina (home, posts, pages, etc.)
|
||||
- **AND** DEBE evaluar exclusiones por categoria, ID, URL pattern
|
||||
- **AND** NO DEBE evaluar `hide_for_logged_in`
|
||||
- **BECAUSE** esa evaluacion la hace el cliente
|
||||
|
||||
#### Scenario: Componente sin JavaScript-First usa metodo existente
|
||||
- **GIVEN** un componente sin `javascript_first_mode`
|
||||
- **WHEN** se llama `PageVisibilityHelper::shouldShow('otro-componente')`
|
||||
- **THEN** DEBE usar flujo existente incluyendo `hide_for_logged_in`
|
||||
- **BECAUSE** sin JS-First, PHP es la unica fuente de verdad
|
||||
|
||||
---
|
||||
|
||||
### Requirement 3: WordPressComponentVisibilityRepository debe soportar modo bypass
|
||||
|
||||
The repository MUST support skipping `is_user_logged_in()` check when requested.
|
||||
|
||||
#### Scenario: isNotExcluded con bypass de login check
|
||||
- **GIVEN** se llama `isNotExcluded('adsense-placement', skipLoginCheck: true)`
|
||||
- **WHEN** el metodo evalua exclusiones
|
||||
- **THEN** NO DEBE llamar `is_user_logged_in()`
|
||||
- **AND** DEBE evaluar otras exclusiones normalmente
|
||||
|
||||
---
|
||||
|
||||
### Requirement 4: El endpoint REST DEBE evaluar hide_for_logged_in
|
||||
|
||||
The REST endpoint `/adsense-placement/visibility` MUST evaluate `hide_for_logged_in`
|
||||
and include it in the decision.
|
||||
|
||||
**NOTA:** Esto YA esta implementado en `AdsenseVisibilityController.php`. Solo documentamos
|
||||
para claridad.
|
||||
|
||||
#### Scenario: Endpoint evalua usuario logueado
|
||||
- **GIVEN** peticion al endpoint con cookies de sesion
|
||||
- **AND** `hide_for_logged_in` esta habilitado
|
||||
- **AND** el usuario ESTA logueado
|
||||
- **WHEN** el endpoint procesa la peticion
|
||||
- **THEN** retorna `show_ads: false`
|
||||
- **AND** `reasons` incluye `hide_for_logged_in`
|
||||
|
||||
---
|
||||
|
||||
### Requirement 5: El CSS de slots ocultos NO debe afectar el layout
|
||||
|
||||
When JavaScript hides ad slots, they MUST collapse completely without affecting page layout.
|
||||
|
||||
**NOTA:** Esto YA esta implementado con `:has([data-ad-status='unfilled'])`. Documentamos para
|
||||
verificar que se mantiene.
|
||||
|
||||
---
|
||||
|
||||
## Implementation
|
||||
|
||||
### Archivos a modificar
|
||||
|
||||
#### 1. AdsensePlacementRenderer.php
|
||||
|
||||
```php
|
||||
// ANTES (linea 38-43):
|
||||
public function renderSlot(array $settings, string $location): string
|
||||
{
|
||||
// 0. Verificar visibilidad por tipo de pagina y exclusiones
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// DESPUES:
|
||||
public function renderSlot(array $settings, string $location): string
|
||||
{
|
||||
// 0. Verificar visibilidad (respetando modo JS-First para cache)
|
||||
$jsFirstMode = ($settings['behavior']['javascript_first_mode'] ?? false) === true;
|
||||
|
||||
if ($jsFirstMode) {
|
||||
// En modo JS-First, usar evaluacion cache-friendly (sin is_user_logged_in)
|
||||
if (!PageVisibilityHelper::shouldShowForCache('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
// Modo legacy: usar evaluacion completa
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. PageVisibilityHelper.php
|
||||
|
||||
```php
|
||||
// AGREGAR nuevo metodo:
|
||||
/**
|
||||
* Evalua visibilidad SIN checks dependientes de usuario
|
||||
*
|
||||
* Para uso cuando JavaScript manejara los checks de usuario.
|
||||
* Evalua: tipos de pagina, exclusiones por categoria/ID/URL
|
||||
* NO evalua: hide_for_logged_in
|
||||
*
|
||||
* @param string $componentName
|
||||
* @return bool
|
||||
*/
|
||||
public static function shouldShowForCache(string $componentName): bool
|
||||
{
|
||||
$container = DIContainer::getInstance();
|
||||
$useCase = $container->getEvaluateComponentVisibilityUseCase();
|
||||
|
||||
return $useCase->executeForCache($componentName);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. EvaluateComponentVisibilityUseCase.php
|
||||
|
||||
```php
|
||||
// AGREGAR nuevo metodo:
|
||||
/**
|
||||
* Evalua visibilidad SIN checks dependientes de usuario
|
||||
*/
|
||||
public function executeForCache(string $componentName): bool
|
||||
{
|
||||
// Paso 1: Verificar visibilidad por tipo de pagina (sin cambios)
|
||||
$visibleByPageType = $this->pageVisibilityUseCase->execute($componentName);
|
||||
|
||||
if (!$visibleByPageType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Paso 2: Verificar exclusiones SIN hide_for_logged_in
|
||||
$isExcluded = $this->exclusionsUseCase->executeForCache($componentName);
|
||||
|
||||
return !$isExcluded;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. EvaluateExclusionsUseCase.php
|
||||
|
||||
```php
|
||||
// AGREGAR nuevo metodo:
|
||||
/**
|
||||
* Evalua exclusiones SIN hide_for_logged_in
|
||||
*/
|
||||
public function executeForCache(string $componentName): bool
|
||||
{
|
||||
// Evaluar exclusiones por categoria, ID, URL
|
||||
// NO evaluar hide_for_logged_in
|
||||
return $this->repository->isExcludedForCache($componentName);
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. WordPressComponentVisibilityRepository.php
|
||||
|
||||
```php
|
||||
// MODIFICAR isNotExcluded o agregar nuevo metodo:
|
||||
/**
|
||||
* Verifica exclusiones SIN evaluar hide_for_logged_in
|
||||
*/
|
||||
public function isNotExcludedForCache(string $componentName): bool
|
||||
{
|
||||
// OMITE: shouldHideForLoggedIn()
|
||||
// MANTIENE: PageVisibilityHelper::shouldShow() para otras exclusiones
|
||||
|
||||
return PageVisibilityHelper::shouldShowByPageType($componentName);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Con `javascript_first_mode=true` y `hide_for_logged_in=true`:
|
||||
- Usuario anonimo: HTML generado, JS muestra ads
|
||||
- Usuario logueado: HTML generado, JS oculta ads
|
||||
- Page cache sirve mismo HTML a ambos
|
||||
|
||||
2. Con `javascript_first_mode=false`:
|
||||
- Comportamiento legacy sin cambios
|
||||
- `is_user_logged_in()` se evalua en PHP
|
||||
|
||||
3. Otras exclusiones (categoria, ID, URL) funcionan igual en ambos modos
|
||||
|
||||
4. No hay regresion en visibilidad por tipo de pagina
|
||||
|
||||
5. Los slots ocultos por JS colapsan completamente (height: 0)
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- **NO hay breaking changes**: `shouldShow()` mantiene comportamiento actual
|
||||
- **Nuevo metodo opcional**: `shouldShowForCache()` para modo JS-First
|
||||
- **Backward compatible**: Si `javascript_first_mode=false`, todo funciona igual
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2025-12-11 | Initial spec |
|
||||
214
_openspec/changes/adsense-cache-unified-visibility/test-plan.md
Normal file
214
_openspec/changes/adsense-cache-unified-visibility/test-plan.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Test Plan: Unificacion de Visibilidad AdSense para Cache
|
||||
|
||||
## Test Environment
|
||||
|
||||
- **DEV URL**: https://dev.analisisdepreciosunitarios.com
|
||||
- **PROD URL**: https://analisisdepreciosunitarios.com
|
||||
- **Browsers**: Chrome, Firefox
|
||||
- **Tools**: DevTools Console, Network tab, Playwright MCP
|
||||
|
||||
---
|
||||
|
||||
## Pre-requisitos
|
||||
|
||||
1. Habilitar `javascript_first_mode` en settings de adsense-placement
|
||||
2. Habilitar `hide_for_logged_in` en settings de adsense-placement
|
||||
3. Tener cuenta de usuario para pruebas de login
|
||||
4. Limpiar cache del sitio antes de cada prueba
|
||||
|
||||
---
|
||||
|
||||
## Test Cases
|
||||
|
||||
### TC01: HTML se genera para usuarios anonimos (JS-First activo)
|
||||
|
||||
**Objetivo**: Verificar que los slots de ads se renderizan en HTML para visitantes anonimos
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario anonimo |
|
||||
| Pasos | 1. Limpiar cache del sitio<br>2. Abrir pagina de post en navegacion privada<br>3. Inspeccionar HTML |
|
||||
| Resultado esperado | Elementos `.roi-adsense-placeholder` presentes en DOM |
|
||||
| Status | PENDING |
|
||||
| Notas | |
|
||||
|
||||
### TC02: HTML se genera para usuarios logueados (JS-First activo)
|
||||
|
||||
**Objetivo**: Verificar que los slots de ads se renderizan en HTML incluso para usuarios logueados
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario logueado |
|
||||
| Pasos | 1. Limpiar cache del sitio<br>2. Login como usuario<br>3. Visitar pagina de post<br>4. Inspeccionar HTML |
|
||||
| Resultado esperado | Elementos `.roi-adsense-placeholder` presentes en DOM (mismo HTML que TC01) |
|
||||
| Status | PENDING |
|
||||
| Notas | Clave: el HTML debe ser IDENTICO al de TC01 |
|
||||
|
||||
### TC03: Endpoint retorna show_ads=false para usuarios logueados
|
||||
|
||||
**Objetivo**: Verificar que el endpoint REST evalua correctamente hide_for_logged_in
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario logueado |
|
||||
| Pasos | 1. Login como usuario<br>2. En console ejecutar:<br>`fetch('/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=1', {credentials:'same-origin'}).then(r=>r.json()).then(console.log)` |
|
||||
| Resultado esperado | Respuesta: `{show_ads: false, reasons: ['hide_for_logged_in'], ...}` |
|
||||
| Status | PENDING |
|
||||
| Notas | |
|
||||
|
||||
### TC04: JavaScript oculta ads para usuarios logueados
|
||||
|
||||
**Objetivo**: Verificar que el JavaScript aplica correctamente la decision del endpoint
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario logueado |
|
||||
| Pasos | 1. Login como usuario<br>2. Visitar pagina de post<br>3. Verificar clases CSS de slots<br>4. Verificar evento `roiAdsenseDeactivated` |
|
||||
| Resultado esperado | Slots tienen clase `roi-adsense-hidden` o altura colapsada |
|
||||
| Status | PENDING |
|
||||
| Notas | |
|
||||
|
||||
### TC05: JavaScript muestra ads para usuarios anonimos
|
||||
|
||||
**Objetivo**: Verificar que el JavaScript muestra correctamente los ads
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Pre-condicion | `javascript_first_mode=true`, `hide_for_logged_in=true`, usuario anonimo |
|
||||
| Pasos | 1. Navegacion privada<br>2. Visitar pagina de post<br>3. Verificar clases CSS de slots |
|
||||
| Resultado esperado | Slots tienen clase `roi-adsense-active` |
|
||||
| Status | PENDING |
|
||||
| Notas | |
|
||||
|
||||
### TC06: Cache sirve mismo HTML a ambos tipos de usuario
|
||||
|
||||
**Objetivo**: Verificar compatibilidad con page cache
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Pre-condicion | `javascript_first_mode=true`, page cache activo |
|
||||
| Pasos | 1. Limpiar cache<br>2. Usuario anonimo visita pagina (genera cache)<br>3. Usuario logueado visita misma pagina<br>4. Comparar HTML de `.roi-adsense-placeholder` |
|
||||
| Resultado esperado | HTML identico, diferencia solo en comportamiento JS |
|
||||
| Status | PENDING |
|
||||
| Notas | Este es el test critico de la spec |
|
||||
|
||||
### TC07: Modo legacy funciona sin cambios (JS-First deshabilitado)
|
||||
|
||||
**Objetivo**: Verificar backward compatibility
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Pre-condicion | `javascript_first_mode=false`, `hide_for_logged_in=true`, usuario logueado |
|
||||
| Pasos | 1. Deshabilitar JS-First en settings<br>2. Login como usuario<br>3. Visitar pagina de post<br>4. Inspeccionar HTML |
|
||||
| Resultado esperado | Elementos `.roi-adsense-placeholder` NO presentes (comportamiento legacy) |
|
||||
| Status | PENDING |
|
||||
| Notas | Comportamiento identico al actual |
|
||||
|
||||
### TC08: Exclusiones por tipo de pagina funcionan en ambos modos
|
||||
|
||||
**Objetivo**: Verificar que exclusiones no relacionadas con login siguen funcionando
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Pre-condicion | `show_on_archives=false` configurado |
|
||||
| Pasos | 1. Visitar pagina de archivo (categoria)<br>2. Verificar que no hay slots de ads |
|
||||
| Resultado esperado | Sin slots de ads en paginas de archivo |
|
||||
| Status | PENDING |
|
||||
| Notas | |
|
||||
|
||||
### TC09: Exclusiones por categoria funcionan en ambos modos
|
||||
|
||||
**Objetivo**: Verificar que exclusiones por categoria siguen funcionando
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Pre-condicion | Categoria excluida configurada |
|
||||
| Pasos | 1. Visitar post de categoria excluida<br>2. Verificar que no hay slots de ads |
|
||||
| Resultado esperado | Sin slots de ads en posts de categoria excluida |
|
||||
| Status | PENDING |
|
||||
| Notas | |
|
||||
|
||||
---
|
||||
|
||||
## Automated Test Scripts
|
||||
|
||||
### Script: Verificar HTML identico para cache
|
||||
|
||||
```javascript
|
||||
// Ejecutar en Playwright o consola
|
||||
// Compara DOM de slots entre usuario anonimo y logueado
|
||||
|
||||
async function verifyCacheCompatibility() {
|
||||
const anonSlots = document.querySelectorAll('.roi-adsense-placeholder').length;
|
||||
console.log(`Slots encontrados: ${anonSlots}`);
|
||||
|
||||
// El HTML debe existir independientemente del estado de login
|
||||
// La diferencia esta en las clases CSS aplicadas por JS
|
||||
const activeSlots = document.querySelectorAll('.roi-adsense-active').length;
|
||||
const hiddenSlots = document.querySelectorAll('.roi-adsense-hidden').length;
|
||||
|
||||
console.log(`Activos: ${activeSlots}, Ocultos: ${hiddenSlots}`);
|
||||
|
||||
return {
|
||||
totalSlots: anonSlots,
|
||||
active: activeSlots,
|
||||
hidden: hiddenSlots
|
||||
};
|
||||
}
|
||||
|
||||
verifyCacheCompatibility();
|
||||
```
|
||||
|
||||
### Script: Verificar respuesta del endpoint
|
||||
|
||||
```javascript
|
||||
// Ejecutar en consola con usuario logueado
|
||||
async function testEndpoint() {
|
||||
const postId = document.querySelector('article')?.id?.replace('post-', '') || '1';
|
||||
const url = `/wp-json/roi-theme/v1/adsense-placement/visibility?post_id=${postId}`;
|
||||
|
||||
const response = await fetch(url, { credentials: 'same-origin' });
|
||||
const data = await response.json();
|
||||
|
||||
console.log('Respuesta del endpoint:');
|
||||
console.log('- show_ads:', data.show_ads);
|
||||
console.log('- reasons:', data.reasons);
|
||||
console.log('- cache_seconds:', data.cache_seconds);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
testEndpoint();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notas de Implementacion
|
||||
|
||||
1. **Orden de ejecucion**: Implementar spec primero, luego ejecutar tests
|
||||
2. **Rollback**: Si falla TC06, revertir cambios (backward compatible)
|
||||
3. **Monitoreo**: Despues de deploy, verificar metricas de AdSense por 24h
|
||||
|
||||
---
|
||||
|
||||
## Results Summary
|
||||
|
||||
| Test Case | Status | Date | Tester |
|
||||
|-----------|--------|------|--------|
|
||||
| TC01 | PENDING | | |
|
||||
| TC02 | PENDING | | |
|
||||
| TC03 | PENDING | | |
|
||||
| TC04 | PENDING | | |
|
||||
| TC05 | PENDING | | |
|
||||
| TC06 | PENDING | | |
|
||||
| TC07 | PENDING | | |
|
||||
| TC08 | PENDING | | |
|
||||
| TC09 | PENDING | | |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2025-12-11 | Initial test plan |
|
||||
2476
_openspec/changes/adsense-javascript-first/spec.md
Normal file
2476
_openspec/changes/adsense-javascript-first/spec.md
Normal file
File diff suppressed because it is too large
Load Diff
1334
_openspec/changes/adsense-javascript-first/test-plan.md
Normal file
1334
_openspec/changes/adsense-javascript-first/test-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
190
_openspec/changes/cache-first-architecture/spec.md
Normal file
190
_openspec/changes/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 |
|
||||
406
_openspec/changes/flujo-componentes/spec.md
Normal file
406
_openspec/changes/flujo-componentes/spec.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# Especificacion de Flujo de Componentes
|
||||
|
||||
## Purpose
|
||||
|
||||
Define el flujo de trabajo de 5 fases para crear componentes en ROITheme, incluyendo convenciones de nomenclatura, estructura de archivos y validacion.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Nomenclatura NO NEGOCIABLE
|
||||
|
||||
The system MUST follow strict naming conventions that are NON-NEGOTIABLE.
|
||||
|
||||
#### Scenario: Nomenclatura de component_name en JSON y BD
|
||||
- **WHEN** se define el nombre del componente en JSON o base de datos
|
||||
- **THEN** DEBE usar kebab-case
|
||||
- **AND** ejemplo: featured-image, hero-section, top-bar
|
||||
|
||||
#### Scenario: Nomenclatura de archivo schema JSON
|
||||
- **WHEN** se crea un archivo de schema JSON
|
||||
- **THEN** el nombre DEBE ser kebab-case
|
||||
- **AND** ejemplo: featured-image.json, hero-section.json
|
||||
|
||||
#### Scenario: Nomenclatura de carpeta de modulo
|
||||
- **WHEN** se crea la carpeta del modulo
|
||||
- **THEN** DEBE usar PascalCase
|
||||
- **AND** ejemplo: FeaturedImage/, HeroSection/, TopBar/
|
||||
|
||||
#### Scenario: Nomenclatura de namespace PHP
|
||||
- **WHEN** se define el namespace PHP
|
||||
- **THEN** DEBE usar PascalCase
|
||||
- **AND** patron: ROITheme\[Contexto]\[Componente]\[Capa]
|
||||
|
||||
#### Scenario: Nomenclatura de clases Renderer y FormBuilder
|
||||
- **WHEN** se nombran las clases Renderer o FormBuilder
|
||||
- **THEN** DEBEN usar PascalCase
|
||||
- **AND** ejemplo: FeaturedImageRenderer, HeroSectionFormBuilder
|
||||
|
||||
#### Scenario: Conversion kebab-case a PascalCase
|
||||
- **WHEN** se convierte de kebab-case a PascalCase
|
||||
- **THEN** se eliminan los guiones
|
||||
- **AND** se capitaliza cada palabra
|
||||
- **AND** ejemplo: featured-image se convierte en FeaturedImage
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 1 - Creacion de Schema JSON
|
||||
|
||||
The first step MUST be creating the component JSON schema.
|
||||
|
||||
#### Scenario: Ubicacion del schema
|
||||
- **WHEN** se crea un schema JSON
|
||||
- **THEN** DEBE colocarse en Schemas/[nombre-en-kebab-case].json
|
||||
- **AND** ejemplo: Schemas/featured-image.json
|
||||
|
||||
#### Scenario: Campo component_name en schema
|
||||
- **WHEN** se define component_name en el schema
|
||||
- **THEN** DEBE usar kebab-case
|
||||
- **AND** ejemplo: component_name con valor featured-image
|
||||
|
||||
#### Scenario: Fuente del schema
|
||||
- **WHEN** se extrae informacion para el schema
|
||||
- **THEN** DEBE basarse en _planificacion/roi-theme-template/index.html
|
||||
- **AND** DEBEN extraerse TODOS los campos CSS y textos del HTML
|
||||
|
||||
#### Scenario: Campos obligatorios de visibilidad
|
||||
- **WHEN** se crea un schema
|
||||
- **THEN** DEBE incluir is_enabled como boolean
|
||||
- **AND** DEBE incluir show_on_desktop como boolean
|
||||
- **AND** DEBE incluir show_on_mobile como boolean
|
||||
|
||||
#### Scenario: Grupos JSON estandar con priorities
|
||||
- **WHEN** se estructura un schema JSON
|
||||
- **THEN** DEBE organizar campos en los 12 grupos estandar con priorities fijas
|
||||
- **AND** VISIBILITY DEBE tener priority 10
|
||||
- **AND** CONTENT DEBE tener priority 20
|
||||
- **AND** TYPOGRAPHY DEBE tener priority 30
|
||||
- **AND** COLORS DEBE tener priority 40
|
||||
- **AND** SPACING DEBE tener priority 50
|
||||
- **AND** VISUAL_EFFECTS DEBE tener priority 60
|
||||
- **AND** BEHAVIOR DEBE tener priority 70
|
||||
- **AND** LAYOUT DEBE tener priority 80
|
||||
- **AND** LINKS, ICONS, MEDIA, FORMS DEBEN tener priority 90
|
||||
|
||||
#### Scenario: Tipos de campo validos en schema
|
||||
- **WHEN** se define un campo en el schema JSON
|
||||
- **THEN** el type DEBE ser uno de: boolean, text, textarea, url, select, color
|
||||
- **AND** si type es select DEBE incluir array de options
|
||||
- **AND** si type es boolean el default DEBE ser true o false
|
||||
- **AND** si type es color el default DEBE ser formato hexadecimal (#RRGGBB)
|
||||
|
||||
#### Scenario: Campo heading_level para semantica HTML
|
||||
- **WHEN** un componente tiene titulo principal
|
||||
- **THEN** el grupo TYPOGRAPHY DEBE incluir campo heading_level
|
||||
- **AND** heading_level DEBE ser tipo select con options h1, h2, h3, h4, h5, h6
|
||||
- **AND** heading_level es critico para jerarquia semantica y SEO
|
||||
|
||||
#### Scenario: Campos de accesibilidad en MEDIA
|
||||
- **WHEN** un componente tiene imagenes
|
||||
- **THEN** el grupo MEDIA DEBE incluir campo image_alt
|
||||
- **AND** image_alt es obligatorio para accesibilidad (WCAG)
|
||||
- **WHEN** la imagen puede compartirse en redes sociales
|
||||
- **THEN** DEBE incluir campo is_og_image como boolean
|
||||
|
||||
#### Scenario: Campos tipicos del grupo BEHAVIOR
|
||||
- **WHEN** un componente tiene comportamiento interactivo
|
||||
- **THEN** el grupo BEHAVIOR puede incluir is_sticky, sticky_offset, collapse_on_mobile
|
||||
- **AND** para Table of Contents puede incluir generate_jump_links, enable_scrollspy
|
||||
- **AND** para animaciones puede incluir animation_type
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 2 - Sincronizacion JSON a BD
|
||||
|
||||
The second step MUST synchronize the JSON schema with the database.
|
||||
|
||||
#### Scenario: Comando de sincronizacion
|
||||
- **WHEN** se necesita sincronizar un componente
|
||||
- **THEN** ejecutar wp roi-theme sync-component [nombre]
|
||||
- **AND** ejemplo: wp roi-theme sync-component featured-image
|
||||
|
||||
#### Scenario: Tabla destino
|
||||
- **WHEN** se sincroniza
|
||||
- **THEN** los datos van a la tabla wp_roi_theme_component_settings
|
||||
|
||||
#### Scenario: Preservacion de valores
|
||||
- **WHEN** se sincroniza un schema actualizado
|
||||
- **THEN** los valores existentes del usuario DEBEN preservarse
|
||||
- **AND** solo se agregan campos nuevos
|
||||
|
||||
#### Scenario: Conversion de valores para almacenamiento
|
||||
- **WHEN** se sincroniza un campo a BD
|
||||
- **THEN** arrays y objects DEBEN convertirse con json_encode()
|
||||
- **AND** booleans DEBEN convertirse a '1' o '0'
|
||||
- **AND** otros tipos DEBEN convertirse a string con cast
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 3 - Creacion del Renderer
|
||||
|
||||
The third step MUST create the Renderer that converts DB data to HTML + CSS.
|
||||
|
||||
#### Scenario: Ubicacion del Renderer
|
||||
- **WHEN** se crea un Renderer
|
||||
- **THEN** DEBE colocarse en Public/[PascalCase]/Infrastructure/Ui/[PascalCase]Renderer.php
|
||||
- **AND** ejemplo: Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
|
||||
|
||||
#### Scenario: Inyeccion de CSSGeneratorInterface
|
||||
- **WHEN** se implementa un Renderer
|
||||
- **THEN** DEBE inyectar CSSGeneratorInterface via constructor
|
||||
- **AND** NO DEBE tener CSS hardcodeado
|
||||
|
||||
#### Scenario: Generacion de CSS
|
||||
- **WHEN** se genera CSS en el Renderer
|
||||
- **THEN** DEBE usar $this->cssGenerator->generate()
|
||||
- **AND** CERO CSS hardcodeado en el codigo PHP
|
||||
|
||||
#### Scenario: Validacion de visibilidad
|
||||
- **WHEN** el Renderer procesa un componente
|
||||
- **THEN** DEBE validar is_enabled, show_on_desktop, show_on_mobile
|
||||
- **AND** NO renderizar si no cumple condiciones de visibilidad
|
||||
|
||||
#### Scenario: Clases responsive Bootstrap para visibilidad
|
||||
- **WHEN** el Renderer genera HTML para show_on_desktop y show_on_mobile
|
||||
- **THEN** DEBE usar clases Bootstrap d-none, d-lg-block, d-lg-none
|
||||
- **AND** NO DEBE usar CSS custom para visibilidad responsive
|
||||
- **AND** el breakpoint principal es lg (992px)
|
||||
|
||||
#### Scenario: Tabla de decision para visibilidad responsive
|
||||
- **GIVEN** campos show_on_desktop y show_on_mobile del componente
|
||||
- **WHEN** show_on_desktop es false AND show_on_mobile es true
|
||||
- **THEN** aplicar clase d-lg-none (solo visible en mobile)
|
||||
- **WHEN** show_on_desktop es true AND show_on_mobile es false
|
||||
- **THEN** aplicar clases d-none d-lg-block (solo visible en desktop)
|
||||
- **WHEN** show_on_desktop es false AND show_on_mobile es false
|
||||
- **THEN** NO renderizar componente y retornar string vacio
|
||||
- **WHEN** show_on_desktop es true AND show_on_mobile es true
|
||||
- **THEN** NO aplicar clases de visibilidad (visible en ambos)
|
||||
|
||||
#### Scenario: Metodo supports
|
||||
- **WHEN** se implementa el metodo supports()
|
||||
- **THEN** DEBE retornar el nombre en kebab-case
|
||||
- **AND** ejemplo: return 'featured-image'
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Contrato de CSSGeneratorInterface
|
||||
|
||||
The CSS generation service MUST follow a specific contract defined in Shared.
|
||||
|
||||
#### Scenario: Ubicacion de CSSGeneratorInterface
|
||||
- **WHEN** se necesita la interface de generacion CSS
|
||||
- **THEN** DEBE estar en Shared/Domain/Contracts/CSSGeneratorInterface.php
|
||||
|
||||
#### Scenario: Firma del metodo generate
|
||||
- **WHEN** se implementa CSSGeneratorInterface
|
||||
- **THEN** DEBE tener metodo generate(string $selector, array $styles): string
|
||||
- **AND** $selector es el selector CSS (ej: '.navbar')
|
||||
- **AND** $styles es array asociativo de propiedades CSS
|
||||
- **AND** retorna string CSS formateado
|
||||
|
||||
#### Scenario: Conversion de propiedades CSS
|
||||
- **WHEN** CSSGeneratorService procesa array de estilos
|
||||
- **THEN** DEBE convertir snake_case a kebab-case
|
||||
- **AND** ejemplo: background_color se convierte en background-color
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 4 - Creacion del FormBuilder
|
||||
|
||||
The fourth step MUST create the FormBuilder for the admin panel.
|
||||
|
||||
#### Scenario: Ubicacion del FormBuilder
|
||||
- **WHEN** se crea un FormBuilder
|
||||
- **THEN** DEBE colocarse en Admin/[PascalCase]/Infrastructure/Ui/[PascalCase]FormBuilder.php
|
||||
- **AND** ejemplo: Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
|
||||
|
||||
#### Scenario: Inyeccion de AdminDashboardRenderer
|
||||
- **WHEN** se implementa un FormBuilder
|
||||
- **THEN** DEBE inyectar AdminDashboardRenderer
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Contrato de AdminDashboardRenderer
|
||||
|
||||
The admin panel rendering service MUST follow a specific contract.
|
||||
|
||||
#### Scenario: Ubicacion de AdminDashboardRenderer
|
||||
- **WHEN** se necesita el renderer del panel admin
|
||||
- **THEN** DEBE estar en Admin/Shared/Infrastructure/Ui/AdminDashboardRenderer.php
|
||||
|
||||
#### Scenario: Responsabilidad de AdminDashboardRenderer
|
||||
- **WHEN** se usa AdminDashboardRenderer
|
||||
- **THEN** DEBE generar el HTML de controles de formulario
|
||||
- **AND** DEBE aplicar el Design System del admin (gradiente, bordes)
|
||||
- **AND** DEBE usar Bootstrap 5 form controls
|
||||
|
||||
#### Scenario: Design System del admin
|
||||
- **WHEN** se implementa la UI del admin
|
||||
- **THEN** DEBE usar gradiente #0E2337 a #1e3a5f
|
||||
- **AND** borde naranja #FF8600
|
||||
- **AND** Bootstrap 5 form controls
|
||||
|
||||
#### Scenario: Registro en getComponents
|
||||
- **WHEN** se registra el FormBuilder
|
||||
- **THEN** DEBE registrarse en getComponents() con ID en kebab-case
|
||||
- **AND** ejemplo: 'featured-image'
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 5 - Validacion de Arquitectura
|
||||
|
||||
The fifth and final step MUST validate the component architecture.
|
||||
|
||||
#### Scenario: Comando de validacion
|
||||
- **WHEN** se necesita validar un componente
|
||||
- **THEN** ejecutar php Shared/Infrastructure/Scripts/validate-architecture.php [nombre]
|
||||
- **AND** ejemplo: php Shared/Infrastructure/Scripts/validate-architecture.php featured-image
|
||||
|
||||
#### Scenario: Elementos validados
|
||||
- **WHEN** se ejecuta la validacion
|
||||
- **THEN** DEBE validar estructura de carpetas
|
||||
- **AND** DEBE validar schema JSON
|
||||
- **AND** DEBE validar datos en BD
|
||||
- **AND** DEBE validar Renderer
|
||||
- **AND** DEBE validar FormBuilder
|
||||
- **AND** DEBE validar cumplimiento SOLID
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Estructura de Archivos por Componente
|
||||
|
||||
A complete component MUST have a specific file structure.
|
||||
|
||||
#### Scenario: Estructura de componente en Public
|
||||
- **GIVEN** un componente FeaturedImage en Public
|
||||
- **WHEN** esta completo
|
||||
- **THEN** DEBE existir Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
|
||||
|
||||
#### Scenario: Estructura de componente en Admin
|
||||
- **GIVEN** un componente FeaturedImage en Admin
|
||||
- **WHEN** esta completo
|
||||
- **THEN** DEBE existir Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
|
||||
|
||||
#### Scenario: Archivo schema
|
||||
- **GIVEN** un componente FeaturedImage
|
||||
- **WHEN** se necesita el schema
|
||||
- **THEN** DEBE existir en Schemas/featured-image.json
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Variables CSS del Tema
|
||||
|
||||
Code MUST use the theme CSS variables.
|
||||
|
||||
#### Scenario: Variables de color disponibles
|
||||
- **WHEN** se necesitan colores
|
||||
- **THEN** DEBEN usarse las variables como --color-navy-dark y --color-orange-primary
|
||||
|
||||
#### Scenario: Prohibicion de CSS hardcodeado
|
||||
- **WHEN** se genera CSS
|
||||
- **THEN** NO DEBE haber colores hardcodeados en PHP
|
||||
- **AND** DEBE usarse CSSGeneratorService
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Reglas NO Negociables del Flujo
|
||||
|
||||
These rules MUST always be followed, without exceptions.
|
||||
|
||||
#### Scenario: Creacion de archivos
|
||||
- **WHEN** se van a crear archivos para un componente
|
||||
- **THEN** DEBE leerse primero el template HTML
|
||||
- **AND** NO crear archivos sin esa referencia
|
||||
|
||||
#### Scenario: Campos de visibilidad
|
||||
- **WHEN** se crea un schema
|
||||
- **THEN** NO DEBE omitirse ninguno de los 3 campos obligatorios de visibilidad
|
||||
|
||||
#### Scenario: CSS en Renderers
|
||||
- **WHEN** se implementa un Renderer
|
||||
- **THEN** NO DEBE haber CSS inline
|
||||
- **AND** todo via CSSGeneratorService
|
||||
|
||||
#### Scenario: Instanciacion de servicios
|
||||
- **WHEN** se necesita un servicio
|
||||
- **THEN** NO instanciar directamente con new Service()
|
||||
- **AND** DEBE usarse Inyeccion de Dependencias
|
||||
|
||||
#### Scenario: Modificacion de campos en BD
|
||||
- **WHEN** se necesita modificar campos
|
||||
- **THEN** NO modificar campos en BD manualmente
|
||||
- **AND** modificar el schema JSON y sincronizar
|
||||
|
||||
#### Scenario: Validacion de arquitectura
|
||||
- **WHEN** se completa un componente
|
||||
- **THEN** NO saltarse la validacion de arquitectura
|
||||
- **AND** ejecutar validate-architecture.php
|
||||
|
||||
#### Scenario: Fases completas
|
||||
- **WHEN** se crea un componente
|
||||
- **THEN** NO crear componentes sin completar las 5 fases
|
||||
- **AND** cada fase es obligatoria
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Comandos WP-CLI
|
||||
|
||||
WP-CLI commands MUST be executed with the correct configuration.
|
||||
|
||||
#### Scenario: Ubicacion de WP-CLI
|
||||
- **WHEN** se necesita ejecutar WP-CLI
|
||||
- **THEN** usar C:\xampp\php_8.0.30_backup\wp-cli.phar
|
||||
|
||||
#### Scenario: Sincronizar un componente
|
||||
- **WHEN** se sincroniza un componente especifico
|
||||
- **THEN** ejecutar powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-component [nombre]"
|
||||
|
||||
#### Scenario: Sincronizar todos los componentes
|
||||
- **WHEN** se sincroniza todo
|
||||
- **THEN** ejecutar powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-all-components"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Flujo de 5 Fases Secuencial
|
||||
|
||||
Component creation MUST follow the exact sequence.
|
||||
|
||||
#### Scenario: Flujo completo de 5 fases
|
||||
- **WHEN** se crea un nuevo componente
|
||||
- **THEN** Fase 1 es crear Schema JSON en Schemas/[nombre].json
|
||||
- **AND** Fase 2 es sincronizar con wp roi-theme sync-component [nombre]
|
||||
- **AND** Fase 3 es crear Renderer en Public/[Nombre]/Infrastructure/Ui/[Nombre]Renderer.php
|
||||
- **AND** Fase 4 es crear FormBuilder en Admin/[Nombre]/Infrastructure/Ui/[Nombre]FormBuilder.php
|
||||
- **AND** Fase 5 es validar con validate-architecture.php [nombre]
|
||||
|
||||
#### Scenario: No saltar fases
|
||||
- **WHEN** se desarrolla un componente
|
||||
- **THEN** NO se DEBE saltar ninguna fase
|
||||
- **AND** cada fase depende de la anterior
|
||||
- **AND** la validacion final es obligatoria
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Checklist de Componente Completo
|
||||
|
||||
A component MUST pass the checklist to be considered complete.
|
||||
|
||||
#### Scenario: Checklist de archivos
|
||||
- **WHEN** se verifica un componente
|
||||
- **THEN** DEBE existir Schemas/[nombre-kebab].json
|
||||
- **AND** DEBE existir Public/[NombrePascal]/Infrastructure/Ui/[NombrePascal]Renderer.php
|
||||
- **AND** DEBE existir Admin/[NombrePascal]/Infrastructure/Ui/[NombrePascal]FormBuilder.php
|
||||
|
||||
#### Scenario: Checklist de codigo
|
||||
- **WHEN** se verifica el codigo
|
||||
- **THEN** schema tiene los 3 campos de visibilidad
|
||||
- **AND** Renderer inyecta CSSGeneratorInterface
|
||||
- **AND** Renderer no tiene CSS hardcodeado
|
||||
- **AND** supports() retorna kebab-case
|
||||
- **AND** FormBuilder registrado con ID kebab-case
|
||||
- **AND** validacion de arquitectura pasa
|
||||
|
||||
#### Scenario: Checklist de BD
|
||||
- **WHEN** se verifica la base de datos
|
||||
- **THEN** datos sincronizados en wp_roi_theme_component_settings
|
||||
- **AND** component_name en kebab-case
|
||||
255
_openspec/changes/patrones-wordpress/spec.md
Normal file
255
_openspec/changes/patrones-wordpress/spec.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Especificacion de Patrones WordPress
|
||||
|
||||
## Purpose
|
||||
|
||||
Define como integrar WordPress con Clean Architecture en ROITheme. WordPress tiene caracteristicas propias que requieren patrones especificos para mantener la arquitectura limpia.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Ubicacion de Hooks de WordPress
|
||||
|
||||
WordPress hooks (add_action, add_filter) MUST be located ONLY in Infrastructure.
|
||||
|
||||
#### Scenario: Hooks prohibidos en Domain
|
||||
- **WHEN** el codigo esta en la capa Domain
|
||||
- **THEN** NO DEBE contener add_action
|
||||
- **AND** NO DEBE contener add_filter
|
||||
- **AND** NO DEBE registrar callbacks de WordPress
|
||||
|
||||
#### Scenario: Hooks prohibidos en Application
|
||||
- **WHEN** el codigo esta en la capa Application
|
||||
- **THEN** NO DEBE contener add_action
|
||||
- **AND** NO DEBE contener add_filter
|
||||
|
||||
#### Scenario: Ubicacion correcta de hooks
|
||||
- **WHEN** se necesitan hooks de WordPress
|
||||
- **THEN** DEBEN colocarse en Infrastructure/Wordpress/[Componente]HooksRegistrar.php
|
||||
- **AND** los hooks DEBEN delegar a Use Cases
|
||||
- **AND** los hooks NO DEBEN contener logica de negocio
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Encapsulacion de wpdb
|
||||
|
||||
Access to global $wpdb MUST be encapsulated in Infrastructure repositories.
|
||||
|
||||
#### Scenario: wpdb prohibido en Domain
|
||||
- **WHEN** el codigo esta en la capa Domain
|
||||
- **THEN** NO DEBE contener global $wpdb
|
||||
- **AND** NO DEBE hacer consultas directas a base de datos
|
||||
|
||||
#### Scenario: wpdb prohibido en Application
|
||||
- **WHEN** el codigo esta en la capa Application
|
||||
- **THEN** NO DEBE contener global $wpdb
|
||||
- **AND** DEBE usar interfaces de repository
|
||||
|
||||
#### Scenario: Ubicacion correcta de wpdb
|
||||
- **WHEN** se necesita acceso a base de datos
|
||||
- **THEN** DEBE usarse en Infrastructure/Persistence/WordPress[Componente]Repository.php
|
||||
- **AND** el repository DEBE implementar una interface definida en Domain
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Ubicacion de wp_enqueue_scripts
|
||||
|
||||
Functions wp_enqueue_style and wp_enqueue_script MUST be located ONLY in Infrastructure.
|
||||
|
||||
#### Scenario: Enqueue prohibido en Domain
|
||||
- **WHEN** el codigo esta en la capa Domain
|
||||
- **THEN** NO DEBE contener wp_enqueue_style
|
||||
- **AND** NO DEBE contener wp_enqueue_script
|
||||
|
||||
#### Scenario: Enqueue prohibido en Application
|
||||
- **WHEN** el codigo esta en la capa Application
|
||||
- **THEN** NO DEBE contener funciones de enqueue
|
||||
- **AND** NO DEBE conocer detalles de assets
|
||||
|
||||
#### Scenario: Ubicacion correcta de enqueue
|
||||
- **WHEN** se necesita cargar assets
|
||||
- **THEN** DEBE colocarse en Infrastructure/Services/[Componente]AssetEnqueuer.php
|
||||
- **AND** DEBE registrarse via hooks en Infrastructure
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Ubicacion de register_post_type
|
||||
|
||||
CPT and taxonomy registration MUST be located ONLY in Infrastructure.
|
||||
|
||||
#### Scenario: CPT no es concepto de Domain
|
||||
- **WHEN** se considera donde registrar un Custom Post Type
|
||||
- **THEN** NO DEBE ir en Domain porque no es concepto de negocio
|
||||
- **AND** NO DEBE ir en Application
|
||||
- **AND** ES un detalle de implementacion de WordPress
|
||||
|
||||
#### Scenario: Ubicacion correcta de CPT
|
||||
- **WHEN** se necesita registrar un Custom Post Type
|
||||
- **THEN** DEBE colocarse en Infrastructure/Wordpress/[Componente]CPTRegistrar.php
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Ubicacion de add_shortcode
|
||||
|
||||
Shortcodes MUST be located ONLY in Infrastructure and delegate to Use Cases.
|
||||
|
||||
#### Scenario: Shortcode prohibido en Domain y Application
|
||||
- **WHEN** el codigo esta en Domain o Application
|
||||
- **THEN** NO DEBE contener add_shortcode
|
||||
|
||||
#### Scenario: Ubicacion correcta de shortcode
|
||||
- **WHEN** se necesita un shortcode
|
||||
- **THEN** DEBE colocarse en Infrastructure/Wordpress/[Componente]ShortcodeRegistrar.php
|
||||
- **AND** DEBE delegar la logica a un Use Case
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Ubicacion de Options API
|
||||
|
||||
WordPress Options API MUST be encapsulated in Infrastructure repositories.
|
||||
|
||||
#### Scenario: Options prohibidas en Domain y Application
|
||||
- **WHEN** el codigo esta en Domain o Application
|
||||
- **THEN** NO DEBE contener get_option
|
||||
- **AND** NO DEBE contener update_option
|
||||
- **AND** NO DEBE contener delete_option
|
||||
|
||||
#### Scenario: Ubicacion correcta de Options
|
||||
- **WHEN** se necesita acceso a opciones de WordPress
|
||||
- **THEN** DEBE colocarse en Infrastructure/Persistence/WordPressSettingsRepository.php
|
||||
- **AND** DEBE implementar una interface de Application
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Evitar functions.php Gigante
|
||||
|
||||
The functions.php file MUST contain only bootstrap, not logic.
|
||||
|
||||
#### Scenario: functions.php correcto
|
||||
- **WHEN** se implementa functions.php
|
||||
- **THEN** DEBE contener solo Autoloader, Container DI y Bootstrap
|
||||
- **AND** NO DEBE contener logica de negocio
|
||||
- **AND** NO DEBE contener definiciones de funciones de negocio
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Evitar Logica en Templates
|
||||
|
||||
Templates MUST contain only rendering, not business logic.
|
||||
|
||||
#### Scenario: Template prohibido con logica
|
||||
- **WHEN** un template de WordPress existe
|
||||
- **THEN** NO DEBE contener global $wpdb
|
||||
- **AND** NO DEBE contener validaciones de negocio
|
||||
- **AND** NO DEBE contener consultas a base de datos
|
||||
- **AND** NO DEBE contener cadenas if/elseif complejas
|
||||
|
||||
#### Scenario: Template correcto
|
||||
- **WHEN** se crea un template
|
||||
- **THEN** DEBE recibir un ViewModel preparado
|
||||
- **AND** DEBE contener solo HTML con escaping
|
||||
- **AND** DEBE usar esc_html(), esc_attr(), esc_url()
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Evitar wpdb Disperso
|
||||
|
||||
Usage of wpdb MUST NOT be scattered throughout the code.
|
||||
|
||||
#### Scenario: wpdb centralizado
|
||||
- **WHEN** se necesita acceso a base de datos
|
||||
- **THEN** DEBE usarse SOLO en repositories
|
||||
- **AND** los repositories DEBEN estar en Infrastructure/Persistence/
|
||||
- **AND** multiples archivos NO DEBEN tener global $wpdb
|
||||
|
||||
#### Scenario: Beneficios de centralizacion
|
||||
- **WHEN** wpdb esta centralizado en repositories
|
||||
- **THEN** cambiar la base de datos requiere modificar solo repositories
|
||||
- **AND** el testing es posible via mocks de interface
|
||||
- **AND** la logica de negocio permanece pura
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Evitar Variables Globales Masivas
|
||||
|
||||
Code MUST NOT use massive global variables.
|
||||
|
||||
#### Scenario: Globales prohibidas
|
||||
- **WHEN** se escribe codigo
|
||||
- **THEN** NO DEBE usar global $roi_config
|
||||
- **AND** NO DEBE usar singletons dispersos
|
||||
- **AND** NO DEBE crear estado global
|
||||
|
||||
#### Scenario: Solucion con Dependency Injection
|
||||
- **WHEN** se necesitan dependencias compartidas
|
||||
- **THEN** DEBE usarse un Container de Inyeccion de Dependencias
|
||||
- **AND** las dependencias se inyectan via constructor
|
||||
- **AND** no hay estado global
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Codigo Testeable
|
||||
|
||||
Code MUST be testable without WordPress installed.
|
||||
|
||||
#### Scenario: Domain testeable
|
||||
- **WHEN** se escribe codigo de Domain
|
||||
- **THEN** DEBE ser testeable sin base de datos
|
||||
- **AND** DEBE ser testeable sin WordPress
|
||||
- **AND** DEBE ser testeable sin UI
|
||||
|
||||
#### Scenario: Mocks via interfaces
|
||||
- **WHEN** se escriben tests
|
||||
- **THEN** las dependencias se mockean via interfaces
|
||||
- **AND** los tests unitarios NO requieren setup complejo
|
||||
- **AND** Domain puede probarse de forma aislada
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Arbol de Decisiones WordPress
|
||||
|
||||
WordPress code MUST be located according to code type.
|
||||
|
||||
#### Scenario: Determinar ubicacion de hooks
|
||||
- **WHEN** el codigo es add_action o add_filter
|
||||
- **THEN** va en Infrastructure/Wordpress/[Componente]HooksRegistrar.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de wpdb
|
||||
- **WHEN** el codigo usa global $wpdb
|
||||
- **THEN** va en Infrastructure/Persistence/WordPress[Componente]Repository.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de enqueue
|
||||
- **WHEN** el codigo usa wp_enqueue_style o wp_enqueue_script
|
||||
- **THEN** va en Infrastructure/Services/[Componente]AssetEnqueuer.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de CPT
|
||||
- **WHEN** el codigo usa register_post_type o register_taxonomy
|
||||
- **THEN** va en Infrastructure/Wordpress/[Componente]CPTRegistrar.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de shortcode
|
||||
- **WHEN** el codigo usa add_shortcode
|
||||
- **THEN** va en Infrastructure/Wordpress/[Componente]ShortcodeRegistrar.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de options
|
||||
- **WHEN** el codigo usa get_option o update_option
|
||||
- **THEN** va en Infrastructure/Persistence/WordPressSettingsRepository.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de nonces
|
||||
- **WHEN** el codigo usa wp_nonce_field o check_admin_referer
|
||||
- **THEN** va en Infrastructure/Api/Wordpress/[Componente]Controller.php
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Comandos de Validacion WordPress
|
||||
|
||||
Code MUST be validated with specific commands.
|
||||
|
||||
#### Scenario: Validar Domain sin WordPress
|
||||
- **WHEN** se valida la capa Domain
|
||||
- **THEN** grep por global $wpdb DEBE retornar vacio
|
||||
- **AND** grep por add_action DEBE retornar vacio
|
||||
- **AND** grep por $_POST DEBE retornar vacio
|
||||
|
||||
#### Scenario: Validar Application sin WordPress
|
||||
- **WHEN** se valida la capa Application
|
||||
- **THEN** grep por global $wpdb DEBE retornar vacio
|
||||
- **AND** grep por add_action DEBE retornar vacio
|
||||
- **AND** grep por wp_enqueue DEBE retornar vacio
|
||||
410
_openspec/changes/post-grid-shortcode/spec.md
Normal file
410
_openspec/changes/post-grid-shortcode/spec.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# Especificacion de Shortcode Post Grid
|
||||
|
||||
## Purpose
|
||||
|
||||
Crear un shortcode `[roi_post_grid]` que permita insertar grids de posts en cualquier pagina o entrada de WordPress, con filtros configurables por categoria, tag, autor y otras opciones. Sigue Clean Architecture delegando logica a Use Cases.
|
||||
|
||||
## Context
|
||||
|
||||
### Problema Actual
|
||||
El componente `post-grid` implementado en `templates-unificados` solo funciona en templates de archivo (home.php, archive.php, etc.) porque depende del loop principal de WordPress (`global $wp_query`).
|
||||
|
||||
### Solucion
|
||||
Crear un shortcode que:
|
||||
1. Siga Clean Architecture (ShortcodeRegistrar → UseCase → Repository)
|
||||
2. Reutilice estilos del componente `post-grid` existente
|
||||
3. Permita filtrar posts por categoria, tag, autor
|
||||
4. No dependa del loop principal de WordPress
|
||||
|
||||
### Relacion con templates-unificados
|
||||
- Este shortcode **complementa** (no reemplaza) el componente post-grid
|
||||
- El componente post-grid sigue funcionando en templates de archivo
|
||||
- El shortcode permite insertar grids en contenido arbitrario
|
||||
|
||||
### Nota sobre shortcodes legacy
|
||||
Los shortcodes existentes (`apu_table`, `apu_row`) estan en `Inc/apu-tables.php` (patron legacy). Este nuevo shortcode sigue el patron moderno de Clean Architecture.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Arquitectura del Shortcode
|
||||
|
||||
The shortcode MUST follow Clean Architecture patterns.
|
||||
|
||||
#### Scenario: Ubicacion del ShortcodeRegistrar
|
||||
- **WHEN** se implementa el shortcode
|
||||
- **THEN** DEBE estar en `Shared/Infrastructure/Wordpress/PostGridShortcodeRegistrar.php`
|
||||
- **AND** DEBE usar namespace `ROITheme\Shared\Infrastructure\Wordpress`
|
||||
- **AND** DEBE registrar el shortcode via add_shortcode
|
||||
|
||||
#### Scenario: Delegacion a Use Case
|
||||
- **WHEN** el shortcode se ejecuta
|
||||
- **THEN** PostGridShortcodeRegistrar DEBE delegar a RenderPostGridUseCase
|
||||
- **AND** NO DEBE contener logica de negocio
|
||||
- **AND** solo sanitiza atributos y pasa al Use Case
|
||||
|
||||
#### Scenario: Ubicacion del Use Case
|
||||
- **WHEN** se implementa la logica del shortcode
|
||||
- **THEN** DEBE estar en `Shared/Application/UseCases/RenderPostGrid/RenderPostGridUseCase.php`
|
||||
- **AND** DEBE usar namespace `ROITheme\Shared\Application\UseCases\RenderPostGrid`
|
||||
- **AND** DEBE orquestar Query, Renderer y Settings
|
||||
|
||||
#### Scenario: Ubicacion del QueryBuilder
|
||||
- **WHEN** se construye el WP_Query
|
||||
- **THEN** DEBE estar en `Shared/Infrastructure/Query/PostGridQueryBuilder.php`
|
||||
- **AND** DEBE usar namespace `ROITheme\Shared\Infrastructure\Query`
|
||||
- **AND** DEBE recibir parametros y retornar WP_Query
|
||||
|
||||
#### Scenario: Ubicacion del ShortcodeRenderer
|
||||
- **WHEN** se genera el HTML del grid
|
||||
- **THEN** DEBE estar en `Shared/Infrastructure/Ui/PostGridShortcodeRenderer.php`
|
||||
- **AND** DEBE usar namespace `ROITheme\Shared\Infrastructure\Ui`
|
||||
- **AND** DEBE inyectar CSSGeneratorInterface
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Estructura de Clases
|
||||
|
||||
Each class MUST have single responsibility following SRP.
|
||||
|
||||
#### Scenario: Responsabilidad de PostGridShortcodeRegistrar
|
||||
- **WHEN** se define PostGridShortcodeRegistrar
|
||||
- **THEN** DEBE tener metodo estatico `register()` para add_shortcode
|
||||
- **AND** DEBE tener metodo `handleShortcode(array $atts): string`
|
||||
- **AND** handleShortcode DEBE sanitizar atributos
|
||||
- **AND** handleShortcode DEBE obtener UseCase del DIContainer
|
||||
- **AND** handleShortcode DEBE retornar resultado del UseCase
|
||||
- **AND** NO DEBE tener mas de 50 lineas
|
||||
|
||||
#### Scenario: Responsabilidad de RenderPostGridUseCase
|
||||
- **WHEN** se define RenderPostGridUseCase
|
||||
- **THEN** DEBE recibir RenderPostGridRequest como input
|
||||
- **AND** DEBE inyectar PostGridQueryBuilderInterface via constructor
|
||||
- **AND** DEBE inyectar PostGridShortcodeRendererInterface via constructor
|
||||
- **AND** DEBE inyectar ComponentSettingsRepositoryInterface via constructor
|
||||
- **AND** DEBE orquestar: obtener settings, construir query, renderizar
|
||||
- **AND** DEBE retornar string HTML
|
||||
- **AND** NO DEBE tener mas de 80 lineas
|
||||
|
||||
#### Scenario: Responsabilidad de PostGridQueryBuilder
|
||||
- **WHEN** se define PostGridQueryBuilder
|
||||
- **THEN** DEBE recibir parametros de filtro (category, tag, etc.)
|
||||
- **AND** DEBE construir array de argumentos WP_Query
|
||||
- **AND** DEBE retornar WP_Query configurado
|
||||
- **AND** NO DEBE generar HTML
|
||||
- **AND** NO DEBE tener mas de 100 lineas
|
||||
|
||||
#### Scenario: Responsabilidad de PostGridShortcodeRenderer
|
||||
- **WHEN** se define PostGridShortcodeRenderer
|
||||
- **THEN** DEBE recibir WP_Query y configuracion
|
||||
- **AND** DEBE inyectar CSSGeneratorInterface
|
||||
- **AND** DEBE generar HTML del grid
|
||||
- **AND** DEBE generar CSS inline
|
||||
- **AND** NO DEBE construir queries
|
||||
- **AND** NO DEBE tener mas de 150 lineas
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Interfaces en Domain/Application
|
||||
|
||||
Interfaces MUST be defined for dependency injection.
|
||||
|
||||
#### Scenario: Interface del QueryBuilder
|
||||
- **WHEN** se define la interface
|
||||
- **THEN** DEBE estar en `Shared/Domain/Contracts/PostGridQueryBuilderInterface.php`
|
||||
- **AND** DEBE tener metodo `build(array $params): \WP_Query`
|
||||
|
||||
#### Scenario: Interface del Renderer
|
||||
- **WHEN** se define la interface
|
||||
- **THEN** DEBE estar en `Shared/Domain/Contracts/PostGridShortcodeRendererInterface.php`
|
||||
- **AND** DEBE tener metodo `render(\WP_Query $query, array $settings, array $options): string`
|
||||
|
||||
#### Scenario: Request DTO
|
||||
- **WHEN** se define el DTO de entrada
|
||||
- **THEN** DEBE estar en `Shared/Application/UseCases/RenderPostGrid/RenderPostGridRequest.php`
|
||||
- **AND** DEBE ser readonly class con propiedades tipadas
|
||||
- **AND** DEBE contener todos los atributos del shortcode
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Atributos del Shortcode
|
||||
|
||||
The shortcode MUST accept configurable attributes.
|
||||
|
||||
#### Scenario: Atributo category
|
||||
- **WHEN** se usa `[roi_post_grid category="precios-unitarios"]`
|
||||
- **THEN** DEBE filtrar posts por slug de categoria
|
||||
- **AND** DEBE aceptar multiples categorias separadas por coma
|
||||
- **AND** ejemplo: `category="cat1,cat2,cat3"`
|
||||
|
||||
#### Scenario: Atributo exclude_category
|
||||
- **WHEN** se usa `[roi_post_grid exclude_category="noticias"]`
|
||||
- **THEN** DEBE excluir posts de esas categorias
|
||||
- **AND** DEBE aceptar multiples categorias separadas por coma
|
||||
|
||||
#### Scenario: Atributo tag
|
||||
- **WHEN** se usa `[roi_post_grid tag="concreto"]`
|
||||
- **THEN** DEBE filtrar posts por slug de tag
|
||||
- **AND** DEBE aceptar multiples tags separados por coma
|
||||
|
||||
#### Scenario: Atributo author
|
||||
- **WHEN** se usa `[roi_post_grid author="admin"]`
|
||||
- **THEN** DEBE filtrar posts por login de autor
|
||||
- **AND** DEBE aceptar ID numerico o login string
|
||||
|
||||
#### Scenario: Atributo posts_per_page
|
||||
- **WHEN** se usa `[roi_post_grid posts_per_page="6"]`
|
||||
- **THEN** DEBE limitar cantidad de posts mostrados
|
||||
- **AND** default DEBE ser 9
|
||||
- **AND** DEBE aceptar valores entre 1 y 50
|
||||
|
||||
#### Scenario: Atributo columns
|
||||
- **WHEN** se usa `[roi_post_grid columns="4"]`
|
||||
- **THEN** DEBE definir columnas en desktop
|
||||
- **AND** default DEBE ser 3
|
||||
- **AND** DEBE aceptar valores 1, 2, 3 o 4
|
||||
|
||||
#### Scenario: Atributo orderby
|
||||
- **WHEN** se usa `[roi_post_grid orderby="title"]`
|
||||
- **THEN** DEBE ordenar posts por ese campo
|
||||
- **AND** default DEBE ser "date"
|
||||
- **AND** opciones validas: date, title, modified, rand, comment_count
|
||||
|
||||
#### Scenario: Atributo order
|
||||
- **WHEN** se usa `[roi_post_grid order="ASC"]`
|
||||
- **THEN** DEBE definir direccion del orden
|
||||
- **AND** default DEBE ser "DESC"
|
||||
- **AND** opciones validas: ASC, DESC
|
||||
|
||||
#### Scenario: Atributo show_pagination
|
||||
- **WHEN** se usa `[roi_post_grid show_pagination="true"]`
|
||||
- **THEN** DEBE mostrar paginacion si hay mas posts
|
||||
- **AND** default DEBE ser false
|
||||
|
||||
#### Scenario: Atributo offset
|
||||
- **WHEN** se usa `[roi_post_grid offset="3"]`
|
||||
- **THEN** DEBE saltar los primeros N posts
|
||||
- **AND** default DEBE ser 0
|
||||
|
||||
#### Scenario: Atributo exclude_posts
|
||||
- **WHEN** se usa `[roi_post_grid exclude_posts="123,456"]`
|
||||
- **THEN** DEBE excluir posts por ID
|
||||
- **AND** DEBE aceptar IDs separados por coma
|
||||
|
||||
#### Scenario: Atributos de visualizacion
|
||||
- **WHEN** se usan atributos de visualizacion
|
||||
- **THEN** show_thumbnail default true
|
||||
- **AND** show_excerpt default true
|
||||
- **AND** show_meta default true
|
||||
- **AND** show_categories default true
|
||||
- **AND** excerpt_length default 20
|
||||
|
||||
#### Scenario: Atributo class
|
||||
- **WHEN** se usa `[roi_post_grid class="my-custom-grid"]`
|
||||
- **THEN** DEBE agregar clase CSS adicional al contenedor
|
||||
|
||||
#### Scenario: Atributo id para paginacion multiple
|
||||
- **WHEN** se usa `[roi_post_grid id="grid-1" show_pagination="true"]`
|
||||
- **THEN** DEBE usar query var unico `paged_grid-1`
|
||||
- **AND** permite multiples shortcodes paginados en misma pagina
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Obtencion de Settings
|
||||
|
||||
The shortcode MUST obtain styles from post-grid component settings.
|
||||
|
||||
#### Scenario: Lectura de configuracion
|
||||
- **WHEN** RenderPostGridUseCase se ejecuta
|
||||
- **THEN** DEBE usar ComponentSettingsRepositoryInterface
|
||||
- **AND** DEBE obtener settings de componente 'post-grid'
|
||||
- **AND** DEBE aplicar colores, spacing, visual_effects del componente
|
||||
|
||||
#### Scenario: Settings no encontrados
|
||||
- **WHEN** no existen settings de post-grid en BD
|
||||
- **THEN** DEBE usar valores default definidos en VisibilityDefaults
|
||||
- **AND** NO DEBE fallar con error
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Renderizado HTML
|
||||
|
||||
The shortcode MUST generate valid HTML with proper escaping.
|
||||
|
||||
#### Scenario: Estructura HTML del grid
|
||||
- **WHEN** el shortcode renderiza
|
||||
- **THEN** DEBE generar contenedor con clase `roi-post-grid-shortcode`
|
||||
- **AND** DEBE generar row con clase Bootstrap `row`
|
||||
- **AND** cada card DEBE tener columna responsive
|
||||
|
||||
#### Scenario: Clases responsive de columnas
|
||||
- **WHEN** columns es 3
|
||||
- **THEN** cada card DEBE tener `col-12 col-md-6 col-lg-4`
|
||||
- **AND** para columns=4: `col-12 col-md-6 col-lg-3`
|
||||
- **AND** para columns=2: `col-12 col-md-6`
|
||||
- **AND** para columns=1: `col-12`
|
||||
|
||||
#### Scenario: Sin resultados
|
||||
- **WHEN** el query no encuentra posts
|
||||
- **THEN** DEBE mostrar mensaje "No se encontraron publicaciones"
|
||||
- **AND** NO DEBE romper layout
|
||||
|
||||
#### Scenario: Escaping obligatorio
|
||||
- **WHEN** se genera HTML
|
||||
- **THEN** DEBE usar esc_html() para textos
|
||||
- **AND** DEBE usar esc_attr() para atributos
|
||||
- **AND** DEBE usar esc_url() para URLs
|
||||
- **AND** DEBE usar wp_kses_post() para excerpts
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CSS via CSSGenerator
|
||||
|
||||
The shortcode MUST use CSSGeneratorInterface for styles.
|
||||
|
||||
#### Scenario: Generacion de CSS
|
||||
- **WHEN** PostGridShortcodeRenderer genera CSS
|
||||
- **THEN** DEBE inyectar CSSGeneratorInterface
|
||||
- **AND** DEBE usar settings de post-grid desde BD
|
||||
- **AND** DEBE generar CSS inline en el shortcode
|
||||
|
||||
#### Scenario: Selector unico
|
||||
- **WHEN** se genera CSS
|
||||
- **THEN** DEBE usar selector `.roi-post-grid-shortcode`
|
||||
- **AND** si se especifica id, usar `.roi-post-grid-shortcode-{id}`
|
||||
- **AND** NO DEBE conflictuar con post-grid del template
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Registro del Shortcode
|
||||
|
||||
The shortcode MUST be registered in WordPress.
|
||||
|
||||
#### Scenario: Registro en bootstrap
|
||||
- **WHEN** WordPress carga el tema
|
||||
- **THEN** functions-addon.php DEBE llamar PostGridShortcodeRegistrar::register()
|
||||
- **AND** DEBE estar disponible en editor clasico y Gutenberg
|
||||
|
||||
#### Scenario: Metodo register estatico
|
||||
- **WHEN** se llama PostGridShortcodeRegistrar::register()
|
||||
- **THEN** DEBE ejecutar add_shortcode('roi_post_grid', ...)
|
||||
- **AND** DEBE usar DIContainer para obtener dependencias
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Fase 1: Interfaces y DTOs
|
||||
1. Crear `Shared/Domain/Contracts/PostGridQueryBuilderInterface.php`
|
||||
2. Crear `Shared/Domain/Contracts/PostGridShortcodeRendererInterface.php`
|
||||
3. Crear `Shared/Application/UseCases/RenderPostGrid/RenderPostGridRequest.php`
|
||||
|
||||
### Fase 2: Use Case
|
||||
1. Crear `Shared/Application/UseCases/RenderPostGrid/RenderPostGridUseCase.php`
|
||||
2. Implementar orquestacion de query, settings, render
|
||||
|
||||
### Fase 3: Infrastructure - Query
|
||||
1. Crear `Shared/Infrastructure/Query/PostGridQueryBuilder.php`
|
||||
2. Implementar construccion de WP_Query con todos los filtros
|
||||
|
||||
### Fase 4: Infrastructure - Renderer
|
||||
1. Crear `Shared/Infrastructure/Ui/PostGridShortcodeRenderer.php`
|
||||
2. Implementar generacion HTML y CSS
|
||||
|
||||
### Fase 5: Infrastructure - Registrar
|
||||
1. Crear `Shared/Infrastructure/Wordpress/PostGridShortcodeRegistrar.php`
|
||||
2. Implementar registro y sanitizacion de atributos
|
||||
|
||||
### Fase 6: Registro y DI
|
||||
1. Registrar en DIContainer las implementaciones
|
||||
2. Llamar register() en functions-addon.php
|
||||
|
||||
### Fase 7: Testing
|
||||
1. Probar en pagina estatica
|
||||
2. Verificar filtros por categoria, tag
|
||||
3. Verificar paginacion con id unico
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
Shared/
|
||||
├── Domain/
|
||||
│ └── Contracts/
|
||||
│ ├── PostGridQueryBuilderInterface.php
|
||||
│ └── PostGridShortcodeRendererInterface.php
|
||||
├── Application/
|
||||
│ └── UseCases/
|
||||
│ └── RenderPostGrid/
|
||||
│ ├── RenderPostGridRequest.php
|
||||
│ └── RenderPostGridUseCase.php
|
||||
└── Infrastructure/
|
||||
├── Query/
|
||||
│ └── PostGridQueryBuilder.php
|
||||
├── Ui/
|
||||
│ └── PostGridShortcodeRenderer.php
|
||||
└── Wordpress/
|
||||
└── PostGridShortcodeRegistrar.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Ejemplo: Grid basico
|
||||
```
|
||||
[roi_post_grid]
|
||||
```
|
||||
|
||||
### Ejemplo: Posts de una categoria
|
||||
```
|
||||
[roi_post_grid category="precios-unitarios" posts_per_page="6"]
|
||||
```
|
||||
|
||||
### Ejemplo: Multiples shortcodes con paginacion
|
||||
```
|
||||
[roi_post_grid id="grid-cursos" category="cursos" show_pagination="true"]
|
||||
|
||||
[roi_post_grid id="grid-tutoriales" category="tutoriales" show_pagination="true"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Existentes (reutilizar)
|
||||
- `CSSGeneratorInterface` - Para generar CSS
|
||||
- `ComponentSettingsRepositoryInterface` - Para leer config de post-grid
|
||||
- `DIContainer` - Para inyeccion de dependencias
|
||||
- Bootstrap 5 grid system - Para layout responsive
|
||||
|
||||
### Nuevas (crear)
|
||||
- `PostGridQueryBuilderInterface` - Contrato para query builder
|
||||
- `PostGridShortcodeRendererInterface` - Contrato para renderer
|
||||
- `RenderPostGridRequest` - DTO de entrada
|
||||
- `RenderPostGridUseCase` - Orquestador
|
||||
- `PostGridQueryBuilder` - Implementacion query
|
||||
- `PostGridShortcodeRenderer` - Implementacion render
|
||||
- `PostGridShortcodeRegistrar` - Registro WordPress
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Shortcode renderiza sin atributos
|
||||
- [ ] Filtro por categoria funciona
|
||||
- [ ] Filtro por multiples categorias funciona
|
||||
- [ ] Exclusion de categoria funciona
|
||||
- [ ] Filtro por tag funciona
|
||||
- [ ] Columnas 1, 2, 3, 4 funcionan
|
||||
- [ ] Paginacion con id unico funciona
|
||||
- [ ] Multiples shortcodes paginados funcionan
|
||||
- [ ] CSS se aplica desde settings de post-grid
|
||||
- [ ] Mensaje "sin posts" aparece cuando corresponde
|
||||
- [ ] Escaping correcto en todo el HTML
|
||||
- [ ] Funciona en editor clasico
|
||||
- [ ] Funciona en Gutenberg
|
||||
- [ ] No hay errores PHP
|
||||
- [ ] Clases tienen menos de 150 lineas
|
||||
111
_openspec/changes/recaptcha-anti-spam/proposal.md
Normal file
111
_openspec/changes/recaptcha-anti-spam/proposal.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Proposal: reCAPTCHA v3 Anti-Spam Protection
|
||||
|
||||
## Problema
|
||||
|
||||
Los formularios del sitio (Newsletter Footer y Contact Form) carecen de protección CAPTCHA, haciéndolos vulnerables a spam automatizado. Actualmente solo cuentan con:
|
||||
- Nonce de WordPress
|
||||
- Rate limiting básico
|
||||
- Sanitización de inputs
|
||||
- Validación de email
|
||||
|
||||
## Solución Propuesta
|
||||
|
||||
Implementar **Google reCAPTCHA v3** como capa adicional de protección anti-spam.
|
||||
|
||||
### Por qué reCAPTCHA v3
|
||||
|
||||
| Característica | reCAPTCHA v2 | reCAPTCHA v3 |
|
||||
|----------------|--------------|--------------|
|
||||
| UX | Requiere interacción (checkbox/imágenes) | Invisible, sin fricción |
|
||||
| Detección | Binaria (humano/bot) | Score 0.0-1.0 |
|
||||
| Flexibilidad | Fija | Configurable por score |
|
||||
| Impacto en conversión | Negativo | Mínimo |
|
||||
|
||||
### Credenciales
|
||||
|
||||
```
|
||||
Site Key: 6LevZUQsAAAAAB6wcQ4iE6ckaTwgVR_ScBL3vqSj
|
||||
```
|
||||
|
||||
> **Nota**: El Secret Key debe almacenarse en wp-config.php o como opción encriptada en BD, NUNCA en código fuente.
|
||||
|
||||
## Arquitectura Propuesta
|
||||
|
||||
```
|
||||
Shared/
|
||||
├── Domain/
|
||||
│ └── Contracts/
|
||||
│ └── RecaptchaValidatorInterface.php
|
||||
├── Application/
|
||||
│ └── Services/
|
||||
│ └── RecaptchaValidationService.php
|
||||
└── Infrastructure/
|
||||
└── Services/
|
||||
└── GoogleRecaptchaValidator.php
|
||||
```
|
||||
|
||||
### Flujo de Validación
|
||||
|
||||
```
|
||||
1. Frontend: Usuario envía formulario
|
||||
2. Frontend: reCAPTCHA genera token automáticamente
|
||||
3. Backend: AjaxHandler recibe token con datos del form
|
||||
4. Backend: RecaptchaValidationService valida token con API de Google
|
||||
5. Backend: Si score < threshold → rechazar como spam
|
||||
6. Backend: Si score >= threshold → procesar formulario normalmente
|
||||
```
|
||||
|
||||
## Formularios Afectados
|
||||
|
||||
1. **Newsletter Footer** (`Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`)
|
||||
2. **Contact Form** (`Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`)
|
||||
|
||||
## Configuración Administrable
|
||||
|
||||
| Campo | Tipo | Default | Descripción |
|
||||
|-------|------|---------|-------------|
|
||||
| is_enabled | boolean | true | Habilitar/deshabilitar reCAPTCHA |
|
||||
| site_key | text | - | Clave pública de reCAPTCHA |
|
||||
| secret_key | text | - | Clave secreta (encriptada) |
|
||||
| score_threshold | select | 0.5 | Umbral mínimo (0.3, 0.5, 0.7, 0.9) |
|
||||
| action_newsletter | text | newsletter_submit | Acción para newsletter |
|
||||
| action_contact | text | contact_submit | Acción para contacto |
|
||||
|
||||
## Impacto
|
||||
|
||||
### Archivos a Crear
|
||||
- `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
|
||||
- `Shared/Domain/Entities/RecaptchaResult.php`
|
||||
- `Shared/Application/Services/RecaptchaValidationService.php`
|
||||
- `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
|
||||
- `Schemas/recaptcha-settings.json`
|
||||
- `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
|
||||
- `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
|
||||
|
||||
### Archivos a Modificar
|
||||
- `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
|
||||
- `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
|
||||
- `Public/Footer/Infrastructure/Ui/FooterRenderer.php` (agregar script reCAPTCHA)
|
||||
- `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php` (agregar script reCAPTCHA)
|
||||
- `functions.php` (registrar servicios en contenedor DI)
|
||||
- `Admin/Infrastructure/Ui/AdminDashboardRenderer.php` (registrar tab de reCAPTCHA)
|
||||
|
||||
## Riesgos y Mitigaciones
|
||||
|
||||
| Riesgo | Probabilidad | Mitigación |
|
||||
|--------|--------------|------------|
|
||||
| API Google no disponible | Baja | Fallback: permitir envío (fail-open) |
|
||||
| Falsos positivos | Media | Score threshold configurable |
|
||||
| Latencia adicional | Baja | Validación asíncrona, timeout corto |
|
||||
|
||||
## Criterios de Aceptación
|
||||
|
||||
1. reCAPTCHA v3 integrado en ambos formularios
|
||||
2. Score threshold configurable desde admin
|
||||
3. Logging de intentos bloqueados
|
||||
4. Sin impacto visible en UX del usuario
|
||||
5. Fallback funcional si API falla
|
||||
|
||||
## Última actualización
|
||||
|
||||
2025-01-08
|
||||
325
_openspec/changes/recaptcha-anti-spam/spec.md
Normal file
325
_openspec/changes/recaptcha-anti-spam/spec.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# Especificación: reCAPTCHA v3 Anti-Spam Protection
|
||||
|
||||
## Purpose
|
||||
|
||||
Define la integración de Google reCAPTCHA v3 para proteger los formularios del sitio (Newsletter y Contact Form) contra spam automatizado, siguiendo Clean Architecture.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Configuración de reCAPTCHA
|
||||
|
||||
El sistema DEBE permitir configurar reCAPTCHA v3 desde el panel de administración.
|
||||
|
||||
#### Scenario: Schema JSON para configuración
|
||||
- **WHEN** se crea el schema de configuración
|
||||
- **THEN** DEBE ubicarse en `Schemas/recaptcha-settings.json`
|
||||
- **AND** `component_name` DEBE ser `recaptcha-settings`
|
||||
- **AND** DEBE incluir grupo VISIBILITY con `is_enabled`
|
||||
- **AND** DEBE incluir grupo BEHAVIOR con `site_key`, `secret_key`, `score_threshold`
|
||||
|
||||
#### Scenario: Campos obligatorios del schema
|
||||
- **GIVEN** el schema `recaptcha-settings.json`
|
||||
- **WHEN** se define la estructura
|
||||
- **THEN** `is_enabled` DEBE ser tipo boolean con default true
|
||||
- **AND** `site_key` DEBE ser tipo text (clave pública)
|
||||
- **AND** `secret_key` DEBE ser tipo text (clave secreta, almacenada encriptada)
|
||||
- **AND** `score_threshold` DEBE ser tipo select con options: 0.3, 0.5, 0.7, 0.9
|
||||
|
||||
#### Scenario: Sincronización con BD
|
||||
- **WHEN** se sincroniza el schema
|
||||
- **THEN** ejecutar `wp roi-theme sync-component recaptcha-settings`
|
||||
- **AND** los datos DEBEN ir a `wp_roi_theme_component_settings`
|
||||
- **AND** `component_name` en BD DEBE ser `recaptcha-settings`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Contrato de Validación (Domain)
|
||||
|
||||
El Domain DEBE definir la interfaz de validación de reCAPTCHA.
|
||||
|
||||
#### Scenario: Ubicación de RecaptchaValidatorInterface
|
||||
- **WHEN** se crea la interfaz
|
||||
- **THEN** DEBE ubicarse en `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
|
||||
- **AND** namespace DEBE ser `ROITheme\Shared\Domain\Contracts`
|
||||
|
||||
#### Scenario: Firma del método validate
|
||||
- **WHEN** se define RecaptchaValidatorInterface
|
||||
- **THEN** DEBE tener método `validate(string $token, string $action): RecaptchaResult`
|
||||
- **AND** `$token` es el token generado por reCAPTCHA frontend
|
||||
- **AND** `$action` es el nombre de la acción (newsletter_submit, contact_submit)
|
||||
- **AND** retorna objeto `RecaptchaResult` con score y success
|
||||
|
||||
#### Scenario: Entidad RecaptchaResult
|
||||
- **WHEN** se define el resultado de validación
|
||||
- **THEN** DEBE existir `Shared/Domain/Entities/RecaptchaResult.php`
|
||||
- **AND** DEBE tener propiedades: `success` (bool), `score` (float), `action` (string), `errorCodes` (array)
|
||||
- **AND** DEBE tener método `isValid(float $threshold): bool`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Servicio de Aplicación
|
||||
|
||||
La capa Application DEBE orquestar la validación de reCAPTCHA.
|
||||
|
||||
#### Scenario: Ubicación del servicio
|
||||
- **WHEN** se crea el servicio de aplicación
|
||||
- **THEN** DEBE ubicarse en `Shared/Application/Services/RecaptchaValidationService.php`
|
||||
- **AND** namespace DEBE ser `ROITheme\Shared\Application\Services`
|
||||
|
||||
#### Scenario: Inyección de dependencias
|
||||
- **WHEN** se implementa RecaptchaValidationService
|
||||
- **THEN** DEBE inyectar `RecaptchaValidatorInterface` via constructor
|
||||
- **AND** DEBE inyectar `ComponentSettingsRepositoryInterface` para obtener configuración
|
||||
- **AND** NO DEBE instanciar servicios directamente con `new`
|
||||
|
||||
#### Scenario: Lógica de validación con threshold
|
||||
- **GIVEN** un token de reCAPTCHA y una acción
|
||||
- **WHEN** se llama a `validateSubmission(string $token, string $action): bool`
|
||||
- **THEN** DEBE obtener `score_threshold` de la configuración en BD
|
||||
- **AND** DEBE llamar a `RecaptchaValidatorInterface::validate()`
|
||||
- **AND** DEBE retornar `true` si `RecaptchaResult::isValid($threshold)` es true
|
||||
- **AND** DEBE retornar `false` si score está por debajo del threshold
|
||||
|
||||
#### Scenario: Bypass cuando está deshabilitado
|
||||
- **GIVEN** `is_enabled` es false en configuración
|
||||
- **WHEN** se llama a `validateSubmission()`
|
||||
- **THEN** DEBE retornar `true` sin llamar a la API de Google
|
||||
- **AND** NO DEBE generar errores
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Implementación de Infraestructura
|
||||
|
||||
La capa Infrastructure DEBE implementar la comunicación con API de Google.
|
||||
|
||||
#### Scenario: Ubicación del validador
|
||||
- **WHEN** se implementa el validador
|
||||
- **THEN** DEBE ubicarse en `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
|
||||
- **AND** namespace DEBE ser `ROITheme\Shared\Infrastructure\Services`
|
||||
- **AND** DEBE implementar `RecaptchaValidatorInterface`
|
||||
|
||||
#### Scenario: Llamada a API de Google
|
||||
- **WHEN** se valida un token
|
||||
- **THEN** DEBE hacer POST a `https://www.google.com/recaptcha/api/siteverify`
|
||||
- **AND** DEBE enviar `secret` (secret key) y `response` (token)
|
||||
- **AND** DEBE usar `wp_remote_post()` de WordPress
|
||||
- **AND** timeout DEBE ser máximo 5 segundos
|
||||
|
||||
#### Scenario: Parseo de respuesta exitosa
|
||||
- **GIVEN** API de Google responde exitosamente
|
||||
- **WHEN** se parsea la respuesta
|
||||
- **THEN** DEBE extraer `success` (bool)
|
||||
- **AND** DEBE extraer `score` (float 0.0-1.0)
|
||||
- **AND** DEBE extraer `action` (string)
|
||||
- **AND** DEBE retornar `RecaptchaResult` con estos valores
|
||||
|
||||
#### Scenario: Manejo de errores de API
|
||||
- **GIVEN** API de Google no responde o responde con error
|
||||
- **WHEN** se procesa la respuesta
|
||||
- **THEN** DEBE retornar `RecaptchaResult` con `success = false`
|
||||
- **AND** DEBE incluir códigos de error en `errorCodes`
|
||||
- **AND** NO DEBE lanzar excepciones no controladas
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Integración Frontend
|
||||
|
||||
Los Renderers DEBEN incluir el script de reCAPTCHA y generar tokens.
|
||||
|
||||
#### Scenario: Script de reCAPTCHA en FooterRenderer
|
||||
- **WHEN** FooterRenderer genera HTML del newsletter
|
||||
- **AND** reCAPTCHA está habilitado
|
||||
- **THEN** DEBE incluir script: `<script src="https://www.google.com/recaptcha/api.js?render={site_key}"></script>`
|
||||
- **AND** DEBE agregar input hidden `recaptcha_token` al formulario
|
||||
- **AND** DEBE agregar JS para ejecutar `grecaptcha.execute()` al submit
|
||||
|
||||
#### Scenario: Script de reCAPTCHA en ContactFormRenderer
|
||||
- **WHEN** ContactFormRenderer genera HTML del formulario
|
||||
- **AND** reCAPTCHA está habilitado
|
||||
- **THEN** DEBE incluir script de reCAPTCHA con site_key
|
||||
- **AND** DEBE agregar input hidden `recaptcha_token`
|
||||
- **AND** DEBE agregar JS para ejecutar `grecaptcha.execute()` al submit
|
||||
|
||||
#### Scenario: JavaScript de ejecución de reCAPTCHA
|
||||
- **WHEN** usuario hace submit del formulario
|
||||
- **THEN** JS DEBE interceptar el submit
|
||||
- **AND** DEBE llamar `grecaptcha.execute(siteKey, {action: 'action_name'})`
|
||||
- **AND** DEBE esperar el token (Promise)
|
||||
- **AND** DEBE insertar token en input hidden
|
||||
- **AND** DEBE continuar con el submit del formulario
|
||||
|
||||
#### Scenario: No cargar script si está deshabilitado
|
||||
- **GIVEN** reCAPTCHA `is_enabled` es false
|
||||
- **WHEN** se renderiza el formulario
|
||||
- **THEN** NO DEBE incluir script de reCAPTCHA
|
||||
- **AND** NO DEBE agregar input hidden de token
|
||||
- **AND** formulario DEBE funcionar normalmente
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Integración Backend (AjaxHandlers)
|
||||
|
||||
Los AjaxHandlers DEBEN validar el token de reCAPTCHA antes de procesar.
|
||||
|
||||
#### Scenario: Validación en NewsletterAjaxHandler
|
||||
- **WHEN** se procesa suscripción de newsletter
|
||||
- **AND** reCAPTCHA está habilitado
|
||||
- **THEN** DEBE obtener `recaptcha_token` del POST
|
||||
- **AND** DEBE llamar a `RecaptchaValidationService::validateSubmission()`
|
||||
- **AND** si retorna false, DEBE responder con error JSON
|
||||
- **AND** si retorna true, DEBE continuar procesamiento normal
|
||||
|
||||
#### Scenario: Validación en ContactFormAjaxHandler
|
||||
- **WHEN** se procesa envío de formulario de contacto
|
||||
- **AND** reCAPTCHA está habilitado
|
||||
- **THEN** DEBE obtener `recaptcha_token` del POST
|
||||
- **AND** DEBE llamar a `RecaptchaValidationService::validateSubmission()`
|
||||
- **AND** si retorna false, DEBE responder con error JSON
|
||||
- **AND** si retorna true, DEBE continuar procesamiento normal
|
||||
|
||||
#### Scenario: Mensaje de error por reCAPTCHA fallido
|
||||
- **GIVEN** validación de reCAPTCHA falla
|
||||
- **WHEN** AjaxHandler responde
|
||||
- **THEN** DEBE retornar JSON con `success: false`
|
||||
- **AND** mensaje DEBE ser genérico: "No se pudo verificar que eres humano. Intenta de nuevo."
|
||||
- **AND** NO DEBE revelar detalles técnicos del score
|
||||
|
||||
#### Scenario: Inyección de dependencias en AjaxHandlers
|
||||
- **WHEN** se modifican los AjaxHandlers
|
||||
- **THEN** DEBEN inyectar `RecaptchaValidationService` via constructor
|
||||
- **AND** NO DEBE instanciar servicios directamente
|
||||
- **AND** DEBE seguir principio de Inversión de Dependencias
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Panel de Administración
|
||||
|
||||
DEBE existir un FormBuilder para configurar reCAPTCHA.
|
||||
|
||||
#### Scenario: Ubicación del FormBuilder
|
||||
- **WHEN** se crea el FormBuilder
|
||||
- **THEN** DEBE ubicarse en `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
|
||||
- **AND** namespace DEBE ser `ROITheme\Admin\RecaptchaSettings\Infrastructure\Ui`
|
||||
|
||||
#### Scenario: Registro en getComponents
|
||||
- **WHEN** se registra el FormBuilder
|
||||
- **THEN** DEBE registrarse en `getComponents()` con ID `recaptcha-settings`
|
||||
- **AND** DEBE aparecer en el menú del admin dashboard
|
||||
|
||||
#### Scenario: Campos del formulario admin
|
||||
- **WHEN** se renderiza el formulario de configuración
|
||||
- **THEN** DEBE mostrar toggle para `is_enabled`
|
||||
- **AND** DEBE mostrar input text para `site_key`
|
||||
- **AND** DEBE mostrar input password para `secret_key`
|
||||
- **AND** DEBE mostrar select para `score_threshold` con opciones 0.3, 0.5, 0.7, 0.9
|
||||
- **AND** DEBE seguir Design System: gradiente #0E2337 → #1e3a5f, borde #FF8600
|
||||
|
||||
#### Scenario: FieldMapper para mapeo de campos
|
||||
- **WHEN** se crea el componente admin
|
||||
- **THEN** DEBE existir `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
|
||||
- **AND** DEBE implementar `FieldMapperInterface`
|
||||
- **AND** DEBE registrarse en `FieldMapperRegistry`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Seguridad
|
||||
|
||||
La implementación DEBE seguir mejores prácticas de seguridad.
|
||||
|
||||
#### Scenario: Almacenamiento de secret key
|
||||
- **WHEN** se guarda el secret key
|
||||
- **THEN** DEBE almacenarse encriptado en BD
|
||||
- **AND** NO DEBE aparecer en código fuente
|
||||
- **AND** NO DEBE exponerse en frontend
|
||||
|
||||
#### Scenario: Site key en frontend
|
||||
- **GIVEN** site key es público por diseño de Google
|
||||
- **WHEN** se incluye en frontend
|
||||
- **THEN** PUEDE incluirse en atributo de script
|
||||
- **AND** DEBE obtenerse de configuración en BD
|
||||
|
||||
#### Scenario: Validación de token en backend
|
||||
- **WHEN** se recibe token de reCAPTCHA
|
||||
- **THEN** DEBE sanitizarse con `sanitize_text_field()`
|
||||
- **AND** DEBE validarse que no esté vacío
|
||||
- **AND** DEBE enviarse a API de Google para verificación real
|
||||
|
||||
#### Scenario: No confiar solo en frontend
|
||||
- **GIVEN** tokens pueden ser fabricados
|
||||
- **WHEN** se valida reCAPTCHA
|
||||
- **THEN** SIEMPRE DEBE verificarse con API de Google en backend
|
||||
- **AND** NUNCA confiar en validación solo de frontend
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Logging y Monitoreo
|
||||
|
||||
El sistema DEBE registrar intentos de spam bloqueados.
|
||||
|
||||
#### Scenario: Log de intentos bloqueados
|
||||
- **WHEN** reCAPTCHA bloquea un intento
|
||||
- **THEN** DEBE registrar en log de WordPress
|
||||
- **AND** DEBE incluir: timestamp, IP, action, score obtenido, threshold configurado
|
||||
- **AND** NO DEBE incluir datos personales del usuario
|
||||
|
||||
#### Scenario: Log de errores de API
|
||||
- **WHEN** API de Google falla
|
||||
- **THEN** DEBE registrar error en log
|
||||
- **AND** DEBE incluir código de error y mensaje
|
||||
- **AND** DEBE permitir diagnóstico del problema
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fallback y Resiliencia
|
||||
|
||||
El sistema DEBE manejar fallos graciosamente.
|
||||
|
||||
#### Scenario: Fail-open cuando API no responde
|
||||
- **GIVEN** API de Google no responde (timeout)
|
||||
- **WHEN** se intenta validar
|
||||
- **THEN** DEBE permitir el envío del formulario (fail-open)
|
||||
- **AND** DEBE registrar el evento en log
|
||||
- **AND** NO DEBE bloquear usuarios legítimos por falla de terceros
|
||||
|
||||
#### Scenario: Degradación cuando reCAPTCHA deshabilitado
|
||||
- **GIVEN** administrador deshabilita reCAPTCHA
|
||||
- **WHEN** se envía formulario
|
||||
- **THEN** DEBE procesarse normalmente
|
||||
- **AND** validaciones existentes (nonce, rate limit) DEBEN seguir activas
|
||||
- **AND** NO DEBE generar errores por ausencia de token
|
||||
|
||||
---
|
||||
|
||||
## Checklist de Implementación
|
||||
|
||||
### Archivos a Crear
|
||||
- [ ] `Schemas/recaptcha-settings.json`
|
||||
- [ ] `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
|
||||
- [ ] `Shared/Domain/Entities/RecaptchaResult.php`
|
||||
- [ ] `Shared/Application/Services/RecaptchaValidationService.php`
|
||||
- [ ] `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
|
||||
- [ ] `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
|
||||
- [ ] `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
|
||||
|
||||
### Archivos a Modificar
|
||||
- [ ] `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
|
||||
- [ ] `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
|
||||
- [ ] `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
|
||||
- [ ] `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
|
||||
- [ ] `functions.php` (registro DI)
|
||||
- [ ] `Admin/Infrastructure/Ui/AdminDashboardRenderer.php` (registrar componente)
|
||||
- [ ] `Admin/Shared/Infrastructure/FieldMapping/FieldMapperRegistry.php`
|
||||
|
||||
### Validaciones
|
||||
- [ ] Schema tiene campos de visibilidad
|
||||
- [ ] Domain no tiene dependencias de Infrastructure
|
||||
- [ ] Application solo depende de interfaces de Domain
|
||||
- [ ] Todos los servicios inyectados via constructor
|
||||
- [ ] CSS generado via CSSGeneratorService (si aplica)
|
||||
- [ ] Secret key nunca expuesto en frontend
|
||||
|
||||
---
|
||||
|
||||
## Última actualización
|
||||
|
||||
2025-01-08
|
||||
132
_openspec/changes/recaptcha-anti-spam/tasks.md
Normal file
132
_openspec/changes/recaptcha-anti-spam/tasks.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Tasks: reCAPTCHA v3 Anti-Spam Protection
|
||||
|
||||
## Fase 1: Especificación
|
||||
- [x] Crear proposal.md
|
||||
- [x] Crear tasks.md
|
||||
- [x] Crear spec.md con formato Gherkin
|
||||
- [ ] Obtener aprobación del usuario
|
||||
|
||||
## Fase 2: Implementación
|
||||
|
||||
### 2.1 Capa Domain (Contratos y Entidades)
|
||||
- [ ] Crear `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
|
||||
```php
|
||||
interface RecaptchaValidatorInterface {
|
||||
public function validate(string $token, string $action): RecaptchaResult;
|
||||
}
|
||||
```
|
||||
- [ ] Crear `Shared/Domain/Entities/RecaptchaResult.php`
|
||||
```php
|
||||
final class RecaptchaResult {
|
||||
public function __construct(
|
||||
private bool $success,
|
||||
private float $score,
|
||||
private string $action,
|
||||
private array $errorCodes = []
|
||||
) {}
|
||||
public function isValid(float $threshold): bool;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Capa Application (Servicios)
|
||||
- [ ] Crear `Shared/Application/Services/RecaptchaValidationService.php`
|
||||
- Orquestar validación
|
||||
- Aplicar threshold configurable
|
||||
- Logging de resultados
|
||||
|
||||
### 2.3 Capa Infrastructure (Implementación)
|
||||
- [ ] Crear `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
|
||||
- Llamada HTTP a API de Google
|
||||
- Manejo de errores y timeout
|
||||
- Parseo de respuesta JSON
|
||||
|
||||
### 2.4 Schema y Admin UI
|
||||
- [ ] Crear `Schemas/recaptcha-settings.json`
|
||||
- Campos: is_enabled, site_key, secret_key, score_threshold, actions
|
||||
- [ ] Sincronizar schema con BD: `wp roi-theme sync-component recaptcha-settings`
|
||||
- [ ] Crear `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
|
||||
- [ ] Crear `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
|
||||
- [ ] Registrar en `getComponents()` del AdminDashboardRenderer
|
||||
- [ ] Registrar FieldMapper en FieldMapperRegistry
|
||||
|
||||
### 2.5 Integración Frontend
|
||||
- [ ] Modificar `FooterRenderer.php`
|
||||
- Agregar script de reCAPTCHA con site key
|
||||
- Modificar form para incluir token hidden
|
||||
- [ ] Modificar `ContactFormRenderer.php`
|
||||
- Agregar script de reCAPTCHA con site key
|
||||
- Modificar form para incluir token hidden
|
||||
- [ ] Crear JS compartido para ejecutar reCAPTCHA y obtener token
|
||||
|
||||
### 2.6 Integración Backend
|
||||
- [ ] Modificar `NewsletterAjaxHandler.php`
|
||||
- Inyectar RecaptchaValidationService
|
||||
- Validar token antes de procesar
|
||||
- Retornar error si score bajo
|
||||
- [ ] Modificar `ContactFormAjaxHandler.php`
|
||||
- Inyectar RecaptchaValidationService
|
||||
- Validar token antes de procesar
|
||||
- Retornar error si score bajo
|
||||
|
||||
### 2.7 Registro DI
|
||||
- [ ] Modificar `functions.php`
|
||||
- Registrar RecaptchaValidatorInterface → GoogleRecaptchaValidator
|
||||
- Registrar RecaptchaValidationService
|
||||
|
||||
## Fase 3: Integración y Validación
|
||||
|
||||
### 3.1 Testing Manual
|
||||
- [ ] Probar Newsletter con reCAPTCHA habilitado
|
||||
- [ ] Probar Contact Form con reCAPTCHA habilitado
|
||||
- [ ] Probar con reCAPTCHA deshabilitado (fallback)
|
||||
- [ ] Probar cambio de threshold desde admin
|
||||
- [ ] Verificar logging de intentos
|
||||
|
||||
### 3.2 Validación de Arquitectura
|
||||
- [ ] Ejecutar `validate-architecture.php recaptcha-settings`
|
||||
- [ ] Verificar cumplimiento Clean Architecture
|
||||
- [ ] Verificar inyección de dependencias correcta
|
||||
|
||||
### 3.3 Documentación
|
||||
- [ ] Actualizar CLAUDE.md si es necesario
|
||||
- [ ] Documentar configuración en admin
|
||||
|
||||
## Dependencias
|
||||
|
||||
| Tarea | Depende de |
|
||||
|-------|------------|
|
||||
| Application Service | Domain Contract |
|
||||
| Infrastructure Service | Domain Contract |
|
||||
| Admin FormBuilder | Schema JSON sincronizado |
|
||||
| Frontend integration | Site Key configurado |
|
||||
| Backend integration | Application Service + Infrastructure |
|
||||
|
||||
## Estimación de Archivos
|
||||
|
||||
| Tipo | Cantidad |
|
||||
|------|----------|
|
||||
| Nuevos | 7 |
|
||||
| Modificados | 7 |
|
||||
| Total | 14 |
|
||||
|
||||
### Archivos Nuevos
|
||||
1. `Shared/Domain/Contracts/RecaptchaValidatorInterface.php`
|
||||
2. `Shared/Domain/Entities/RecaptchaResult.php`
|
||||
3. `Shared/Application/Services/RecaptchaValidationService.php`
|
||||
4. `Shared/Infrastructure/Services/GoogleRecaptchaValidator.php`
|
||||
5. `Schemas/recaptcha-settings.json`
|
||||
6. `Admin/RecaptchaSettings/Infrastructure/Ui/RecaptchaSettingsFormBuilder.php`
|
||||
7. `Admin/RecaptchaSettings/Infrastructure/FieldMapping/RecaptchaSettingsFieldMapper.php`
|
||||
|
||||
### Archivos a Modificar
|
||||
1. `Public/Footer/Infrastructure/Api/WordPress/NewsletterAjaxHandler.php`
|
||||
2. `Public/ContactForm/Infrastructure/Api/WordPress/ContactFormAjaxHandler.php`
|
||||
3. `Public/Footer/Infrastructure/Ui/FooterRenderer.php`
|
||||
4. `Public/ContactForm/Infrastructure/Ui/ContactFormRenderer.php`
|
||||
5. `functions.php`
|
||||
6. `Admin/Infrastructure/Ui/AdminDashboardRenderer.php`
|
||||
7. `Admin/Shared/Infrastructure/FieldMapping/FieldMapperRegistry.php`
|
||||
|
||||
## Última actualización
|
||||
|
||||
2025-01-08
|
||||
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
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -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
417
_openspec/changes/templates-unificados/spec.md
Normal file
417
_openspec/changes/templates-unificados/spec.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# Especificacion de Templates Unificados para Blog/Archive
|
||||
|
||||
## Purpose
|
||||
|
||||
Define la arquitectura para unificar todos los templates de listados (blog, categorias, tags, archives) usando la misma estructura que `single.php`, aprovechando el sistema de visibilidad existente para controlar que componentes mostrar en cada contexto. Incluye la creacion de dos nuevos componentes: `archive-header` y `post-grid`.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Template Unificado para Listados
|
||||
|
||||
All listing templates MUST use the same structure as single.php.
|
||||
|
||||
#### Scenario: Estructura base de templates de listado
|
||||
- **WHEN** se implementa home.php, archive.php, category.php o tag.php
|
||||
- **THEN** DEBE usar la misma estructura que single.php
|
||||
- **AND** DEBE llamar a roi_render_component() para cada componente
|
||||
- **AND** la visibilidad se controla via PageVisibilityHelper::shouldShow()
|
||||
|
||||
#### Scenario: Componentes que se llaman en templates de listado
|
||||
- **WHEN** se renderiza un template de listado
|
||||
- **THEN** DEBE llamar a roi_render_component('hero')
|
||||
- **AND** DEBE llamar a roi_render_component('archive-header')
|
||||
- **AND** DEBE llamar a roi_render_component('post-grid')
|
||||
- **AND** DEBE llamar a roi_render_component('table-of-contents') en sidebar
|
||||
- **AND** DEBE llamar a roi_render_component('cta-box-sidebar') en sidebar
|
||||
- **AND** DEBE llamar a roi_render_component('contact-form')
|
||||
- **AND** cada componente decide si renderiza segun show_on_archives
|
||||
|
||||
#### Scenario: Determinacion de sidebar en listados
|
||||
- **WHEN** se determina si mostrar sidebar en un listado
|
||||
- **THEN** DEBE usar roi_should_render_any_wrapper(['table-of-contents', 'cta-box-sidebar'])
|
||||
- **AND** si retorna true usar col-lg-9 para contenido principal
|
||||
- **AND** si retorna false usar col-lg-12 para contenido principal
|
||||
|
||||
#### Scenario: Paginacion en templates de listado
|
||||
- **WHEN** se muestra paginacion en un listado
|
||||
- **THEN** DEBE usar the_posts_pagination() de WordPress
|
||||
- **AND** DEBE aplicar estilos Bootstrap via CSS del componente post-grid
|
||||
|
||||
#### Scenario: CSS de paginacion generado por post-grid
|
||||
- **WHEN** PostGridRenderer renderiza la paginacion
|
||||
- **THEN** el CSS de paginacion DEBE generarse via CSSGeneratorService
|
||||
- **AND** DEBE aplicar estilos Bootstrap (nav-links, page-numbers)
|
||||
- **AND** los colores DEBEN ser configurables via grupo colors del schema
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Componente archive-header
|
||||
|
||||
The archive-header component MUST display dynamic title and description for archive pages.
|
||||
|
||||
#### Scenario: Ubicacion de archivos archive-header
|
||||
- **WHEN** se crea el componente archive-header
|
||||
- **THEN** schema DEBE estar en Schemas/archive-header.json
|
||||
- **AND** Renderer DEBE estar en Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php
|
||||
- **AND** FormBuilder DEBE estar en Admin/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderFormBuilder.php
|
||||
- **AND** FieldMapper DEBE estar en Admin/ArchiveHeader/Infrastructure/FieldMapping/ArchiveHeaderFieldMapper.php
|
||||
|
||||
#### Scenario: Namespaces PHP de archive-header
|
||||
- **WHEN** se definen los namespaces para archive-header
|
||||
- **THEN** Renderer DEBE usar namespace `ROITheme\Public\ArchiveHeader\Infrastructure\Ui`
|
||||
- **AND** FormBuilder DEBE usar namespace `ROITheme\Admin\ArchiveHeader\Infrastructure\Ui`
|
||||
- **AND** FieldMapper DEBE usar namespace `ROITheme\Admin\ArchiveHeader\Infrastructure\FieldMapping`
|
||||
|
||||
#### Scenario: Deteccion automatica de tipo de archivo
|
||||
- **WHEN** ArchiveHeaderRenderer detecta el tipo de pagina
|
||||
- **THEN** para categoria DEBE mostrar "Categoria: [nombre]"
|
||||
- **AND** para tag DEBE mostrar "Etiqueta: [nombre]"
|
||||
- **AND** para autor DEBE mostrar "Articulos de: [nombre]"
|
||||
- **AND** para fecha DEBE mostrar "Archivo: [Mes Ano]"
|
||||
- **AND** para busqueda DEBE mostrar "Resultados para: [termino]"
|
||||
- **AND** para blog home DEBE mostrar el valor de blog_title del schema
|
||||
|
||||
#### Scenario: Grupos del schema archive-header
|
||||
- **WHEN** se define el schema archive-header.json
|
||||
- **THEN** DEBE incluir grupo visibility con priority 10
|
||||
- **AND** DEBE incluir grupo content con priority 20
|
||||
- **AND** DEBE incluir grupo typography con priority 30
|
||||
- **AND** DEBE incluir grupo colors con priority 40
|
||||
- **AND** DEBE incluir grupo spacing con priority 50
|
||||
- **AND** DEBE incluir grupo behavior con priority 70
|
||||
- **NOTE** archive-header NO incluye visual_effects (priority 60) porque es un componente de texto simple sin sombras, bordes redondeados ni transiciones hover
|
||||
|
||||
#### Scenario: Campos obligatorios de visibility en archive-header
|
||||
- **WHEN** se define grupo visibility en schema
|
||||
- **THEN** DEBE incluir is_enabled como boolean con default true
|
||||
- **AND** DEBE incluir show_on_desktop como boolean con default true
|
||||
- **AND** DEBE incluir show_on_mobile como boolean con default true
|
||||
|
||||
#### Scenario: Campos de _page_visibility en archive-header
|
||||
- **WHEN** se configura visibilidad por tipo de pagina en FieldMapper
|
||||
- **THEN** DEBE mapear campo show_on_home en grupo _page_visibility con default false
|
||||
- **AND** DEBE mapear campo show_on_posts en grupo _page_visibility con default false
|
||||
- **AND** DEBE mapear campo show_on_pages en grupo _page_visibility con default false
|
||||
- **AND** DEBE mapear campo show_on_archives en grupo _page_visibility con default true
|
||||
- **AND** DEBE mapear campo show_on_search en grupo _page_visibility con default false
|
||||
- **NOTE** Los campos _page_visibility NO van en el schema JSON, se manejan via FieldMapper y VisibilityDefaults
|
||||
- **NOTE** show_on_archives en true porque este componente solo tiene sentido en archives
|
||||
|
||||
#### Scenario: Campos de content en archive-header
|
||||
- **WHEN** se define grupo content
|
||||
- **THEN** DEBE incluir blog_title como text con default "Blog"
|
||||
- **AND** DEBE incluir show_post_count como boolean con default true
|
||||
- **AND** DEBE incluir show_description como boolean con default true
|
||||
|
||||
#### Scenario: Campos de typography en archive-header
|
||||
- **WHEN** se define grupo typography
|
||||
- **THEN** DEBE incluir heading_level como select con options ["h1", "h2", "h3", "h4", "h5", "h6"] y default "h1"
|
||||
- **AND** DEBE incluir title_size como text con default "2rem"
|
||||
- **AND** DEBE incluir title_weight como text con default "700"
|
||||
- **AND** DEBE incluir description_size como text con default "1rem"
|
||||
|
||||
#### Scenario: Campos de colors en archive-header
|
||||
- **WHEN** se define grupo colors
|
||||
- **THEN** DEBE incluir title_color como color con default "#0E2337"
|
||||
- **AND** DEBE incluir description_color como color con default "#6b7280"
|
||||
- **AND** DEBE incluir count_bg_color como color con default "#FF8600"
|
||||
- **AND** DEBE incluir count_text_color como color con default "#ffffff"
|
||||
|
||||
#### Scenario: Campos de spacing en archive-header
|
||||
- **WHEN** se define grupo spacing
|
||||
- **THEN** DEBE incluir margin_top como text con default "2rem"
|
||||
- **AND** DEBE incluir margin_bottom como text con default "2rem"
|
||||
- **AND** DEBE incluir padding como text con default "1.5rem"
|
||||
|
||||
#### Scenario: Campos de behavior en archive-header
|
||||
- **WHEN** se define grupo behavior
|
||||
- **THEN** DEBE incluir is_sticky como boolean con default false
|
||||
- **AND** DEBE incluir sticky_offset como text con default "0"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Componente post-grid
|
||||
|
||||
The post-grid component MUST display posts from the main WordPress loop in a grid layout.
|
||||
|
||||
#### Scenario: Ubicacion de archivos post-grid
|
||||
- **WHEN** se crea el componente post-grid
|
||||
- **THEN** schema DEBE estar en Schemas/post-grid.json
|
||||
- **AND** Renderer DEBE estar en Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php
|
||||
- **AND** FormBuilder DEBE estar en Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php
|
||||
- **AND** FieldMapper DEBE estar en Admin/PostGrid/Infrastructure/FieldMapping/PostGridFieldMapper.php
|
||||
|
||||
#### Scenario: Namespaces PHP de post-grid
|
||||
- **WHEN** se definen los namespaces para post-grid
|
||||
- **THEN** Renderer DEBE usar namespace `ROITheme\Public\PostGrid\Infrastructure\Ui`
|
||||
- **AND** FormBuilder DEBE usar namespace `ROITheme\Admin\PostGrid\Infrastructure\Ui`
|
||||
- **AND** FieldMapper DEBE usar namespace `ROITheme\Admin\PostGrid\Infrastructure\FieldMapping`
|
||||
|
||||
#### Scenario: Diferencia entre post-grid y related-post
|
||||
- **WHEN** PostGridRenderer obtiene los posts
|
||||
- **THEN** DEBE usar global $wp_query para obtener posts del loop principal
|
||||
- **AND** NO DEBE crear su propio WP_Query como hace RelatedPostRenderer
|
||||
- **AND** DEBE llamar wp_reset_postdata() al finalizar si modifica el loop
|
||||
|
||||
#### Scenario: Grupos del schema post-grid
|
||||
- **WHEN** se define el schema post-grid.json
|
||||
- **THEN** DEBE incluir grupo visibility con priority 10
|
||||
- **AND** DEBE incluir grupo content con priority 20
|
||||
- **AND** DEBE incluir grupo typography con priority 30
|
||||
- **AND** DEBE incluir grupo colors con priority 40
|
||||
- **AND** DEBE incluir grupo spacing con priority 50
|
||||
- **AND** DEBE incluir grupo visual_effects con priority 60
|
||||
- **AND** DEBE incluir grupo layout con priority 80
|
||||
- **AND** DEBE incluir grupo media con priority 90
|
||||
|
||||
#### Scenario: Campos obligatorios de visibility en post-grid
|
||||
- **WHEN** se define grupo visibility en schema
|
||||
- **THEN** DEBE incluir is_enabled como boolean con default true
|
||||
- **AND** DEBE incluir show_on_desktop como boolean con default true
|
||||
- **AND** DEBE incluir show_on_mobile como boolean con default true
|
||||
|
||||
#### Scenario: Campos de _page_visibility en post-grid
|
||||
- **WHEN** se configura visibilidad por tipo de pagina en FieldMapper
|
||||
- **THEN** DEBE mapear campo show_on_home en grupo _page_visibility con default true
|
||||
- **AND** DEBE mapear campo show_on_posts en grupo _page_visibility con default false
|
||||
- **AND** DEBE mapear campo show_on_pages en grupo _page_visibility con default false
|
||||
- **AND** DEBE mapear campo show_on_archives en grupo _page_visibility con default true
|
||||
- **AND** DEBE mapear campo show_on_search en grupo _page_visibility con default true
|
||||
- **NOTE** Los campos _page_visibility NO van en el schema JSON, se manejan via FieldMapper y VisibilityDefaults
|
||||
- **NOTE** show_on_home en true para mostrar grid en pagina de blog principal
|
||||
- **NOTE** show_on_archives en true porque este componente es para listados
|
||||
- **NOTE** show_on_search en true para mostrar resultados de busqueda
|
||||
|
||||
#### Scenario: Campos de content en post-grid
|
||||
- **WHEN** se define grupo content
|
||||
- **THEN** DEBE incluir show_thumbnail como boolean con default true
|
||||
- **AND** DEBE incluir show_excerpt como boolean con default true
|
||||
- **AND** DEBE incluir show_meta como boolean con default true
|
||||
- **AND** DEBE incluir show_categories como boolean con default true
|
||||
- **AND** DEBE incluir excerpt_length como select con options ["10", "15", "20", "25", "30"] y default "20"
|
||||
- **AND** DEBE incluir read_more_text como text con default "Leer mas"
|
||||
- **AND** DEBE incluir no_posts_message como text con default "No se encontraron publicaciones"
|
||||
|
||||
#### Scenario: Campos de media en post-grid
|
||||
- **WHEN** se define grupo media
|
||||
- **THEN** DEBE incluir fallback_image como url con default ""
|
||||
- **AND** DEBE incluir fallback_image_alt como text con default "Imagen por defecto"
|
||||
- **AND** fallback_image_alt es obligatorio para accesibilidad WCAG
|
||||
|
||||
#### Scenario: Campos de typography en post-grid
|
||||
- **WHEN** se define grupo typography
|
||||
- **THEN** DEBE incluir heading_level como select con options ["h2", "h3", "h4", "h5", "h6"] y default "h3"
|
||||
- **AND** DEBE incluir card_title_size como text con default "1.1rem"
|
||||
- **AND** DEBE incluir card_title_weight como text con default "600"
|
||||
- **AND** DEBE incluir excerpt_size como text con default "0.9rem"
|
||||
- **AND** DEBE incluir meta_size como text con default "0.8rem"
|
||||
|
||||
#### Scenario: Campos de colors en post-grid
|
||||
- **WHEN** se define grupo colors
|
||||
- **THEN** DEBE incluir card_bg_color como color con default "#ffffff"
|
||||
- **AND** DEBE incluir card_title_color como color con default "#0E2337"
|
||||
- **AND** DEBE incluir card_hover_bg_color como color con default "#f9fafb"
|
||||
- **AND** DEBE incluir card_border_color como color con default "#e5e7eb"
|
||||
- **AND** DEBE incluir card_hover_border_color como color con default "#FF8600"
|
||||
- **AND** DEBE incluir excerpt_color como color con default "#6b7280"
|
||||
- **AND** DEBE incluir meta_color como color con default "#9ca3af"
|
||||
- **AND** DEBE incluir category_bg_color como color con default "#FFF5EB"
|
||||
- **AND** DEBE incluir category_text_color como color con default "#FF8600"
|
||||
|
||||
#### Scenario: Campos de spacing en post-grid
|
||||
- **WHEN** se define grupo spacing
|
||||
- **THEN** DEBE incluir grid_gap como text con default "1.5rem"
|
||||
- **AND** DEBE incluir card_padding como text con default "1.25rem"
|
||||
- **AND** DEBE incluir section_margin_top como text con default "0"
|
||||
- **AND** DEBE incluir section_margin_bottom como text con default "2rem"
|
||||
|
||||
#### Scenario: Campos de visual_effects en post-grid
|
||||
- **WHEN** se define grupo visual_effects
|
||||
- **THEN** DEBE incluir card_border_radius como text con default "0.5rem"
|
||||
- **AND** DEBE incluir card_shadow como text con default "0 1px 3px rgba(0,0,0,0.1)"
|
||||
- **AND** DEBE incluir card_hover_shadow como text con default "0 4px 12px rgba(0,0,0,0.15)"
|
||||
- **AND** DEBE incluir card_transition como text con default "all 0.3s ease"
|
||||
- **AND** DEBE incluir image_border_radius como text con default "0.375rem"
|
||||
|
||||
#### Scenario: Campos de layout en post-grid
|
||||
- **WHEN** se define grupo layout
|
||||
- **THEN** DEBE incluir columns_desktop como select con options ["2", "3", "4"] y default "3"
|
||||
- **AND** DEBE incluir columns_tablet como select con options ["1", "2", "3"] y default "2"
|
||||
- **AND** DEBE incluir columns_mobile como select con options ["1", "2"] y default "1"
|
||||
- **AND** DEBE incluir image_position como select con options ["top", "left", "none"] y default "top"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Manejo Graceful de Contenido Faltante
|
||||
|
||||
The post-grid component MUST handle missing content gracefully.
|
||||
|
||||
#### Scenario: Post sin imagen destacada
|
||||
- **WHEN** un post no tiene thumbnail y show_thumbnail es true
|
||||
- **THEN** si fallback_image tiene valor DEBE mostrar esa imagen con fallback_image_alt
|
||||
- **AND** si fallback_image esta vacio DEBE omitir la imagen sin romper el layout
|
||||
- **AND** NO DEBE mostrar imagen rota o placeholder generico
|
||||
|
||||
#### Scenario: Post sin excerpt
|
||||
- **WHEN** un post no tiene excerpt y show_excerpt es true
|
||||
- **THEN** DEBE generar excerpt automatico desde post_content
|
||||
- **AND** DEBE respetar excerpt_length del schema
|
||||
- **AND** DEBE usar wp_trim_words() para truncar
|
||||
|
||||
#### Scenario: Post sin categorias
|
||||
- **WHEN** un post no tiene categorias y show_categories es true
|
||||
- **THEN** DEBE omitir la seccion de categorias
|
||||
- **AND** NO DEBE mostrar "Sin categoria" u otro texto placeholder
|
||||
|
||||
#### Scenario: No posts found - Query vacia
|
||||
- **WHEN** have_posts() retorna false en un template de listado
|
||||
- **THEN** post-grid DEBE mostrar mensaje configurable de "no hay posts"
|
||||
- **AND** el mensaje DEBE usar campo no_posts_message del schema con default "No se encontraron publicaciones"
|
||||
- **AND** DEBE aplicar estilos consistentes con el design system
|
||||
- **AND** NO DEBE romper el layout de la pagina
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Visibilidad por Tipo de Pagina
|
||||
|
||||
Components MUST respect the show_on_archives setting in _page_visibility group.
|
||||
|
||||
#### Scenario: Patron de visibilidad por tipo de pagina
|
||||
- **WHEN** se implementa visibilidad por tipo de pagina
|
||||
- **THEN** los campos show_on_home, show_on_posts, show_on_pages, show_on_archives, show_on_search
|
||||
- **AND** DEBEN estar en grupo _page_visibility (NO en visibility)
|
||||
- **AND** DEBEN mapearse via FieldMapper del componente
|
||||
- **AND** DEBEN evaluarse via PageVisibilityHelper::shouldShow()
|
||||
|
||||
#### Scenario: Configuracion por defecto de show_on_archives para nuevos componentes
|
||||
- **WHEN** se configura _page_visibility para componentes nuevos
|
||||
- **THEN** archive-header DEBE tener show_on_archives true en _page_visibility
|
||||
- **AND** post-grid DEBE tener show_on_archives true en _page_visibility
|
||||
|
||||
#### Scenario: Componentes existentes en archives
|
||||
- **WHEN** se evalua que componentes mostrar en archives via _page_visibility
|
||||
- **THEN** hero DEBE tener show_on_archives false por defecto (configurable)
|
||||
- **AND** table-of-contents DEBE tener show_on_archives false
|
||||
- **AND** featured-image DEBE tener show_on_archives false
|
||||
- **AND** social-share DEBE tener show_on_archives false
|
||||
- **AND** related-post DEBE tener show_on_archives false
|
||||
- **AND** cta-box-sidebar DEBE tener show_on_archives true
|
||||
- **AND** contact-form DEBE tener show_on_archives configurable
|
||||
|
||||
#### Scenario: Llamada a componente con visibilidad deshabilitada (Patron Template Unificado)
|
||||
- **GIVEN** el template unificado llama a TODOS los componentes para mantener consistencia
|
||||
- **WHEN** un template llama roi_render_component() para un componente
|
||||
- **AND** ese componente tiene show_on_archives false
|
||||
- **THEN** el componente NO DEBE renderizarse (retorna string vacio)
|
||||
- **AND** esto es comportamiento correcto y esperado, NO un error
|
||||
- **AND** permite que el admin habilite/deshabilite componentes sin modificar templates
|
||||
- **NOTE** Por ejemplo: table-of-contents se llama en sidebar pero no renderiza en archives porque show_on_archives=false
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Templates a Modernizar
|
||||
|
||||
These templates MUST be updated to use the unified structure.
|
||||
|
||||
#### Scenario: Modernizar home.php
|
||||
- **WHEN** se actualiza home.php
|
||||
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||
- **AND** DEBE usar estructura unificada con hero, archive-header, post-grid
|
||||
|
||||
#### Scenario: Modernizar archive.php
|
||||
- **WHEN** se actualiza archive.php
|
||||
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||
- **AND** DEBE usar estructura unificada
|
||||
|
||||
#### Scenario: Modernizar category.php
|
||||
- **GIVEN** category.php existe en roi-theme/ (verificado)
|
||||
- **WHEN** se actualiza category.php
|
||||
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||
- **AND** DEBE usar estructura unificada
|
||||
|
||||
#### Scenario: Modernizar tag.php
|
||||
- **GIVEN** tag.php existe en roi-theme/ (verificado)
|
||||
- **WHEN** se actualiza tag.php
|
||||
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||
- **AND** DEBE usar estructura unificada
|
||||
|
||||
#### Scenario: Modernizar author.php
|
||||
- **GIVEN** author.php existe en roi-theme/ (verificado)
|
||||
- **WHEN** se actualiza author.php
|
||||
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||
- **AND** DEBE usar estructura unificada
|
||||
- **AND** archive-header detectara automaticamente contexto de autor
|
||||
|
||||
#### Scenario: Modernizar date.php
|
||||
- **GIVEN** date.php existe en roi-theme/ (verificado)
|
||||
- **WHEN** se actualiza date.php
|
||||
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||
- **AND** DEBE usar estructura unificada
|
||||
- **AND** archive-header detectara automaticamente contexto de fecha
|
||||
|
||||
#### Scenario: Modernizar search.php
|
||||
- **GIVEN** search.php existe en roi-theme/ (verificado)
|
||||
- **WHEN** se actualiza search.php
|
||||
- **THEN** DEBE reemplazar get_template_part() con roi_render_component()
|
||||
- **AND** DEBE eliminar referencia a TemplateParts/content.php
|
||||
- **AND** DEBE usar estructura unificada con post-grid
|
||||
- **AND** archive-header detectara automaticamente contexto de busqueda mostrando "Resultados: [termino]"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Orden de Implementacion
|
||||
|
||||
Components and templates MUST be implemented in a specific order.
|
||||
|
||||
#### Scenario: Secuencia de implementacion
|
||||
- **WHEN** se implementa esta especificacion
|
||||
- **THEN** Fase 1 es crear componente archive-header (5 pasos del flujo)
|
||||
- **AND** Fase 2 es crear componente post-grid (5 pasos del flujo)
|
||||
- **AND** Fase 3 es modernizar home.php
|
||||
- **AND** Fase 4 es modernizar archive.php
|
||||
- **AND** Fase 5 es modernizar category.php
|
||||
- **AND** Fase 6 es modernizar tag.php
|
||||
- **AND** Fase 7 es modernizar author.php
|
||||
- **AND** Fase 8 es modernizar date.php
|
||||
- **AND** Fase 9 es modernizar search.php
|
||||
- **AND** Fase 10 es configurar visibilidad de componentes existentes
|
||||
|
||||
#### Scenario: Cada componente sigue flujo de 5 fases
|
||||
- **WHEN** se crea archive-header o post-grid
|
||||
- **THEN** DEBE seguir Fase 1: Schema JSON
|
||||
- **AND** DEBE seguir Fase 2: Sincronizacion wp roi-theme sync-component
|
||||
- **AND** DEBE seguir Fase 3: Renderer
|
||||
- **AND** DEBE seguir Fase 4: FormBuilder
|
||||
- **AND** DEBE seguir Fase 5: Validacion validate-architecture.php
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Dependencias Existentes
|
||||
|
||||
The implementation MUST use existing infrastructure.
|
||||
|
||||
#### Scenario: Uso de PageVisibilityHelper
|
||||
- **WHEN** un Renderer verifica visibilidad
|
||||
- **THEN** DEBE usar PageVisibilityHelper::shouldShow(componentName)
|
||||
- **AND** esta en Shared/Infrastructure/Services/PageVisibilityHelper.php
|
||||
|
||||
#### Scenario: Uso de CSSGeneratorInterface
|
||||
- **WHEN** un Renderer genera CSS
|
||||
- **THEN** DEBE inyectar CSSGeneratorInterface via constructor
|
||||
- **AND** DEBE usar $this->cssGenerator->generate()
|
||||
|
||||
#### Scenario: Uso de roi_should_render_any_wrapper
|
||||
- **WHEN** un template determina si mostrar sidebar
|
||||
- **THEN** DEBE usar roi_should_render_any_wrapper()
|
||||
- **AND** esta definida en functions-addon.php linea 423
|
||||
|
||||
#### Scenario: Uso de DIContainer
|
||||
- **WHEN** se instancian servicios
|
||||
- **THEN** DEBE usar DIContainer::getInstance()
|
||||
- **AND** NO DEBE instanciar servicios con new directamente
|
||||
108
_openspec/project.md
Normal file
108
_openspec/project.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Project Context
|
||||
|
||||
## Purpose
|
||||
ROI Theme es un tema WordPress profesional siguiendo Clean Architecture para el sitio analisisdepreciosunitarios.com. Proporciona un sistema de componentes configurables con panel de administración y renderizado frontend dinámico.
|
||||
|
||||
## Tech Stack
|
||||
- **CMS**: WordPress 6.x
|
||||
- **Lenguaje**: PHP 8.x (strict types)
|
||||
- **Patrón**: Clean Architecture (Domain, Application, Infrastructure)
|
||||
- **Frontend**: Bootstrap 5, Bootstrap Icons
|
||||
- **Base de Datos**: MySQL (tabla normalizada `wp_roi_theme_component_settings`)
|
||||
- **CLI**: WP-CLI para sincronización de schemas
|
||||
|
||||
## Project Conventions
|
||||
|
||||
### Code Style
|
||||
- `declare(strict_types=1)` en todos los archivos PHP
|
||||
- Namespaces: `ROITheme\[Context]\[Component]\[Layer]`
|
||||
- Clases finales por defecto
|
||||
- Propiedades `private`/`protected`
|
||||
- Escaping WordPress: `esc_html()`, `esc_attr()`, `esc_url()`, `esc_textarea()`
|
||||
|
||||
### Nomenclatura (NO NEGOCIABLE)
|
||||
| Contexto | Formato | Ejemplo |
|
||||
|-----------------------------|--------------|----------------------------------|
|
||||
| component_name (JSON/BD) | kebab-case | "featured-image" |
|
||||
| Nombre archivo schema | kebab-case | featured-image.json |
|
||||
| Carpeta de módulo | PascalCase | FeaturedImage/ |
|
||||
| Namespace PHP | PascalCase | ROITheme\Public\FeaturedImage\...|
|
||||
| Clase Renderer/FormBuilder | PascalCase | FeaturedImageRenderer |
|
||||
|
||||
### Architecture Patterns
|
||||
- **Clean Architecture**: Domain → Application → Infrastructure
|
||||
- Domain NO depende de capas superiores
|
||||
- Domain NO puede tener WordPress, echo/print, HTML
|
||||
- Application NO puede tener WordPress
|
||||
- Infrastructure implementa interfaces de Domain
|
||||
- DI via constructor (interfaces, no clases concretas)
|
||||
|
||||
### Estructura del Tema
|
||||
```
|
||||
roi-theme/
|
||||
├── Schemas/ # JSON schemas (kebab-case)
|
||||
├── Shared/ # Código compartido
|
||||
│ ├── Domain/Contracts/ # Interfaces
|
||||
│ ├── Application/UseCases/ # Casos de uso
|
||||
│ └── Infrastructure/ # Implementaciones
|
||||
├── Public/[PascalCase]/ # Renderers frontend
|
||||
├── Admin/[PascalCase]/ # FormBuilders admin
|
||||
└── functions.php # Bootstrap
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
- Validación de arquitectura: `php Shared/Infrastructure/Scripts/validate-architecture.php [nombre]`
|
||||
- Tests unitarios pendientes de implementar
|
||||
|
||||
### Git Workflow
|
||||
- Conventional Commits: `feat:`, `fix:`, `refactor:`, `docs:`
|
||||
- Branch principal: `main`
|
||||
- Tags para releases: `v1.0.0`, `v2.0.0`
|
||||
|
||||
## Domain Context
|
||||
El sistema maneja componentes UI configurables para un sitio de análisis de precios unitarios (construcción). Cada componente tiene:
|
||||
- **Schema JSON**: Define campos configurables
|
||||
- **Renderer**: Genera HTML + CSS dinámico desde BD
|
||||
- **FormBuilder**: Panel admin para configurar valores
|
||||
|
||||
### Flujo de 5 Fases para Componentes
|
||||
|
||||
> **Nota**: `[nombre]` = kebab-case (ej: `contact-form`), `[Nombre]` = PascalCase (ej: `ContactForm`)
|
||||
|
||||
1. Schema JSON → `Schemas/[nombre].json`
|
||||
2. Sincronización → `wp roi-theme sync-component [nombre]`
|
||||
3. Renderer → `Public/[Nombre]/Infrastructure/Ui/[Nombre]Renderer.php`
|
||||
4. FormBuilder → `Admin/[Nombre]/Infrastructure/Ui/[Nombre]FormBuilder.php`
|
||||
5. Validación → `validate-architecture.php [nombre]`
|
||||
|
||||
## Important Constraints
|
||||
- CERO CSS hardcodeado en PHP (usar CSSGeneratorService)
|
||||
- NO usar global $wpdb en Domain/Application
|
||||
- NO instanciar servicios directamente (usar DI)
|
||||
- NO modificar campos en BD manualmente
|
||||
- Variables CSS del tema: `--color-navy-dark`, `--color-orange-primary`
|
||||
|
||||
## External Dependencies
|
||||
- WordPress 6.x core
|
||||
- Bootstrap 5 (CSS/JS)
|
||||
- Bootstrap Icons
|
||||
- WP-CLI (`C:\xampp\php_8.0.30_backup\wp-cli.phar`)
|
||||
|
||||
## Referencias Documentación
|
||||
|
||||
### OpenSpec - Especificaciones del Proyecto
|
||||
| Archivo | Descripción | Ubicación |
|
||||
|---------|-------------|-----------|
|
||||
| **WORKFLOW-ROI-THEME.md** | Flujo de trabajo obligatorio, regla de oro | `_openspec/WORKFLOW-ROI-THEME.md` |
|
||||
| **AGENTS.md** | Agentes disponibles y cuándo usarlos | `_openspec/AGENTS.md` |
|
||||
| **arquitectura-limpia** | Clean Architecture, capas, estructura | `_openspec/specs/arquitectura-limpia.md` |
|
||||
| **estandares-codigo** | SOLID, PHP, WordPress, seguridad | `_openspec/specs/estandares-codigo.md` |
|
||||
| **nomenclatura** | Convenciones de nombres completas | `_openspec/specs/nomenclatura.md` |
|
||||
|
||||
### Documentación de Planificación
|
||||
- Template HTML: `_planificacion/roi-theme-template/index.html`
|
||||
- Design System: `_planificacion/01-design-system/`
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2026-01-08
|
||||
777
_openspec/specs/arquitectura-limpia.md
Normal file
777
_openspec/specs/arquitectura-limpia.md
Normal file
@@ -0,0 +1,777 @@
|
||||
# Especificacion de Arquitectura Limpia
|
||||
|
||||
## Purpose
|
||||
|
||||
Define la implementacion de Clean Architecture para ROITheme, un tema WordPress que sigue principios de Domain-Driven Design con separacion fisica de contextos delimitados (Admin, Public, Shared).
|
||||
|
||||
> **NOTA**: Para convenciones de nomenclatura, ver `_openspec/specs/nomenclatura.md`
|
||||
> **NOTA**: Para principios SOLID y estandares de codigo, ver `_openspec/specs/estandares-codigo.md`
|
||||
> **NOTA**: Para flujo de trabajo y fases obligatorias, ver `_openspec/WORKFLOW-ROI-THEME.md`
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Separacion Fisica de Contextos
|
||||
|
||||
The system MUST organize code into three physically delimited contexts: Admin/, Public/, and Shared/.
|
||||
|
||||
#### Scenario: Codigo pertenece al contexto de administracion
|
||||
- **WHEN** el codigo maneja operaciones CRUD, configuracion o funcionalidad del panel admin
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Admin/`
|
||||
- **AND** el codigo NO DEBE importar del directorio `Public/`
|
||||
|
||||
#### Scenario: Codigo pertenece al contexto publico/frontend
|
||||
- **WHEN** el codigo maneja renderizado, visualizacion o presentacion frontend
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Public/`
|
||||
- **AND** el codigo NO DEBE importar del directorio `Admin/`
|
||||
|
||||
#### Scenario: Codigo es compartido entre contextos
|
||||
- **WHEN** el codigo es usado por AMBOS contextos Admin/ y Public/
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
|
||||
- **AND** tanto Admin/ como Public/ PUEDEN importar de Shared/
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Organizacion Granular de Codigo Compartido
|
||||
|
||||
The system MUST implement three levels of shared code to avoid mixing context-specific shared code.
|
||||
|
||||
#### Scenario: Codigo compartido solo dentro del contexto Admin
|
||||
- **WHEN** el codigo es reutilizado por multiples modulos Admin pero NO por Public
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Admin/Shared/`
|
||||
- **AND** los modulos de Public/ NO DEBEN importar de `Admin/Shared/`
|
||||
|
||||
#### Scenario: Codigo compartido solo dentro del contexto Public
|
||||
- **WHEN** el codigo es reutilizado por multiples modulos Public pero NO por Admin
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Public/Shared/`
|
||||
- **AND** los modulos de Admin/ NO DEBEN importar de `Public/Shared/`
|
||||
|
||||
#### Scenario: Codigo compartido entre ambos contextos
|
||||
- **WHEN** el codigo es reutilizado por AMBOS modulos Admin/ y Public/
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
|
||||
- **AND** esto incluye ValueObjects, Exceptions y Contracts base
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Cada Contexto Sigue las Capas de Clean Architecture
|
||||
|
||||
Each context (Admin/, Public/, Shared/) MUST implement Infrastructure layer, and MAY implement Domain and Application layers when business logic requires it.
|
||||
|
||||
#### Scenario: Estructura de modulo dentro del contexto Admin
|
||||
- **GIVEN** un componente llamado "Navbar" en el contexto Admin
|
||||
- **WHEN** el modulo es creado
|
||||
- **THEN** la estructura DEBE incluir: Admin/Navbar/Infrastructure/
|
||||
- **AND** PUEDE incluir Domain/ (si hay logica de negocio)
|
||||
- **AND** PUEDE incluir Application/ (si hay casos de uso)
|
||||
|
||||
#### Scenario: Estructura de modulo dentro del contexto Public
|
||||
- **GIVEN** un componente llamado "Navbar" en el contexto Public
|
||||
- **WHEN** el modulo es creado
|
||||
- **THEN** la estructura DEBE incluir: Public/Navbar/Infrastructure/
|
||||
- **AND** PUEDE incluir Domain/ (si hay logica de negocio)
|
||||
- **AND** PUEDE incluir Application/ (si hay casos de uso)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Cumplimiento de Direccion de Dependencias
|
||||
|
||||
The system MUST enforce that dependencies flow ONLY from outer layers to inner layers.
|
||||
|
||||
#### Scenario: Infrastructure depende de Application y Domain
|
||||
- **WHEN** el codigo esta en la capa Infrastructure
|
||||
- **THEN** PUEDE importar de la capa Application
|
||||
- **AND** PUEDE importar de la capa Domain
|
||||
|
||||
#### Scenario: Application depende solo de Domain
|
||||
- **WHEN** el codigo esta en la capa Application
|
||||
- **THEN** PUEDE importar de la capa Domain
|
||||
- **AND** NO DEBE importar de la capa Infrastructure
|
||||
|
||||
#### Scenario: Domain no tiene dependencias externas
|
||||
- **WHEN** el codigo esta en la capa Domain
|
||||
- **THEN** NO DEBE importar de la capa Application
|
||||
- **AND** NO DEBE importar de la capa Infrastructure
|
||||
- **AND** NO DEBE importar funciones o globales de WordPress
|
||||
|
||||
---
|
||||
|
||||
### Requirement: La Capa Domain Contiene Solo Logica de Negocio Pura
|
||||
|
||||
The Domain layer MUST contain only pure business logic without framework dependencies.
|
||||
|
||||
#### Scenario: Validacion de contenido de capa Domain
|
||||
- **WHEN** el codigo se coloca en la capa Domain
|
||||
- **THEN** PUEDE contener Entities, Value Objects, Domain Services, Interfaces, Exceptions
|
||||
- **AND** NO DEBE contener global $wpdb, $_POST, $_GET, $_SESSION, add_action, add_filter, HTML, CSS, JavaScript
|
||||
|
||||
#### Scenario: Implementacion de entidad Domain
|
||||
- **GIVEN** una entidad Domain como NavbarConfiguration
|
||||
- **WHEN** la entidad es implementada
|
||||
- **THEN** DEBE contener reglas de negocio y validacion
|
||||
- **AND** NO DEBE contener logica de persistencia
|
||||
- **AND** NO DEBE referenciar APIs de WordPress
|
||||
|
||||
---
|
||||
|
||||
### Requirement: La Capa Application Orquesta Domain
|
||||
|
||||
The Application layer MUST orchestrate domain entities without containing business logic.
|
||||
|
||||
#### Scenario: Implementacion de Use Case
|
||||
- **WHEN** un Use Case es implementado
|
||||
- **THEN** DEBE coordinar entidades y servicios de domain
|
||||
- **AND** DEBE depender de interfaces, NO de implementaciones concretas
|
||||
- **AND** NO DEBE contener reglas de validacion de negocio
|
||||
|
||||
#### Scenario: Uso de DTOs para transferencia de datos
|
||||
- **WHEN** los datos cruzan limites entre capas
|
||||
- **THEN** se DEBEN usar DTOs (Data Transfer Objects)
|
||||
- **AND** los DTOs DEBEN ser contenedores de datos simples sin logica de negocio
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Infrastructure Implementa Interfaces
|
||||
|
||||
The Infrastructure layer MUST implement interfaces defined in Domain/Application layers.
|
||||
|
||||
#### Scenario: Implementacion de Repository
|
||||
- **GIVEN** una RepositoryInterface definida en Domain
|
||||
- **WHEN** el repository es implementado
|
||||
- **THEN** DEBE colocarse en Infrastructure/Persistence/
|
||||
- **AND** DEBE implementar la interface de Domain
|
||||
- **AND** PUEDE usar global $wpdb o APIs de WordPress
|
||||
|
||||
#### Scenario: Integracion con WordPress
|
||||
- **WHEN** se necesita codigo especifico de WordPress
|
||||
- **THEN** DEBE colocarse en la capa Infrastructure
|
||||
- **AND** NO DEBE filtrarse a las capas Domain o Application
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Los Modulos Son Autocontenidos e Independientes
|
||||
|
||||
Each module (Navbar, Footer, Toolbar, etc.) MUST be self-contained and independent from other modules.
|
||||
|
||||
#### Scenario: Aislamiento de modulos
|
||||
- **WHEN** un modulo como Admin/Navbar/ es implementado
|
||||
- **THEN** NO DEBE importar de Admin/Footer/
|
||||
- **AND** NO DEBE importar de Admin/Toolbar/
|
||||
- **AND** SOLO PUEDE importar de Shared/
|
||||
|
||||
#### Scenario: Eliminacion de modulos
|
||||
- **WHEN** un modulo necesita ser eliminado
|
||||
- **THEN** borrar la carpeta del modulo NO DEBE romper otros modulos
|
||||
- **AND** no se DEBERIAN requerir cambios de codigo en otros modulos
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Admin y Public Son Bounded Contexts Separados
|
||||
|
||||
Admin/ and Public/ MUST be treated as separate bounded contexts because they have different responsibilities.
|
||||
|
||||
#### Scenario: Responsabilidad del contexto Admin
|
||||
- **WHEN** el codigo maneja administracion de componentes
|
||||
- **THEN** la entidad Domain se enfoca en configuracion, validacion, estados draft/published
|
||||
- **AND** los Use Cases se enfocan en operaciones Save, Update, Delete, Get
|
||||
|
||||
#### Scenario: Responsabilidad del contexto Public
|
||||
- **WHEN** el codigo maneja renderizado de componentes
|
||||
- **THEN** la entidad Domain se enfoca en estado activo, caching, filtrado por permisos
|
||||
- **AND** los Use Cases se enfocan en operaciones GetActive, Render, Cache
|
||||
|
||||
#### Scenario: No hay duplicacion de domain
|
||||
- **WHEN** Admin/Navbar/Domain/ y Public/Navbar/Domain/ ambos existen
|
||||
- **THEN** NO son duplicados sino bounded contexts especializados
|
||||
- **AND** Admin se enfoca en configuracion/gestion
|
||||
- **AND** Public se enfoca en renderizado/visualizacion
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Validacion de Arquitectura Antes de Commit
|
||||
|
||||
The system MUST validate architectural compliance before committing code.
|
||||
|
||||
#### Scenario: Validacion de capa Domain
|
||||
- **WHEN** se valida codigo de la capa Domain
|
||||
- **THEN** grep por global $wpdb DEBE retornar vacio
|
||||
- **AND** grep por add_action DEBE retornar vacio
|
||||
- **AND** grep por $_POST DEBE retornar vacio
|
||||
|
||||
#### Scenario: Validacion de dependencias de modulos
|
||||
- **WHEN** se validan dependencias entre modulos
|
||||
- **THEN** imports de Admin/Navbar/ desde Admin/Footer/ NO DEBEN existir
|
||||
- **AND** imports de Public/Navbar/ desde Public/Footer/ NO DEBEN existir
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Realizacion de Beneficios de la Arquitectura
|
||||
|
||||
The architecture MUST provide measurable benefits.
|
||||
|
||||
#### Scenario: Asignacion granular de trabajo
|
||||
- **WHEN** un desarrollador es asignado a trabajar en Admin/Navbar/
|
||||
- **THEN** puede acceder SOLO a esa carpeta
|
||||
- **AND** no puede ver ni modificar Public/ u otros modulos de Admin/
|
||||
|
||||
#### Scenario: Eliminacion facil de modulos
|
||||
- **WHEN** un componente ya no es necesario
|
||||
- **THEN** eliminarlo requiere solo borrar la carpeta
|
||||
- **AND** no se necesitan otras modificaciones de codigo
|
||||
|
||||
#### Scenario: Codigo compartido consistente
|
||||
- **WHEN** se encuentra un bug en un ValueObject compartido
|
||||
- **THEN** arreglarlo en Shared/Domain/ValueObjects/ lo arregla para TODOS los modulos
|
||||
- **AND** no se necesita actualizar codigo duplicado
|
||||
|
||||
---
|
||||
|
||||
## Diagrama ASCII de Capas
|
||||
|
||||
```
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ CLEAN ARCHITECTURE ║
|
||||
║ ROI Theme ║
|
||||
╠═══════════════════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
|
||||
║ │ │ ║
|
||||
║ │ INFRASTRUCTURE │ ║
|
||||
║ │ (Capa Externa - WordPress) │ ║
|
||||
║ │ │ ║
|
||||
║ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ ║
|
||||
║ │ │ Ui/ │ │ Api/ │ │ Persistence/│ │ Services/ │ │ ║
|
||||
║ │ │ Renderers │ │ AJAX │ │ Repositories│ │ Helpers │ │ ║
|
||||
║ │ │ FormBuilders│ │ Handlers │ │ $wpdb │ │ │ │ ║
|
||||
║ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ ║
|
||||
║ │ │ ║
|
||||
║ │ PERMITIDO: WordPress APIs, HTML, CSS, JS, $wpdb, hooks │ ║
|
||||
║ │ │ ║
|
||||
║ └─────────────────────────────────────────────────────────────────────┘ ║
|
||||
║ │ ║
|
||||
║ ▼ depende de ║
|
||||
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
|
||||
║ │ │ ║
|
||||
║ │ APPLICATION │ ║
|
||||
║ │ (Casos de Uso / Orquestacion) │ ║
|
||||
║ │ │ ║
|
||||
║ │ ┌─────────────────────────────────────────────────────────────┐ │ ║
|
||||
║ │ │ UseCases/ │ │ ║
|
||||
║ │ │ GetComponentSettingsUseCase, RenderComponentUseCase │ │ ║
|
||||
║ │ │ CheckVisibilityUseCase, SyncSchemaUseCase │ │ ║
|
||||
║ │ └─────────────────────────────────────────────────────────────┘ │ ║
|
||||
║ │ │ ║
|
||||
║ │ PERMITIDO: Orquestacion, DTOs, llamadas a interfaces Domain │ ║
|
||||
║ │ PROHIBIDO: WordPress, HTML, $wpdb, persistencia directa │ ║
|
||||
║ │ │ ║
|
||||
║ └─────────────────────────────────────────────────────────────────────┘ ║
|
||||
║ │ ║
|
||||
║ ▼ depende de ║
|
||||
║ ┌─────────────────────────────────────────────────────────────────────┐ ║
|
||||
║ │ │ ║
|
||||
║ │ DOMAIN │ ║
|
||||
║ │ (Centro - Logica de Negocio Pura) │ ║
|
||||
║ │ │ ║
|
||||
║ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────────────┐ │ ║
|
||||
║ │ │ Entities/ │ │Contracts/ │ │ Value │ │ Exceptions/ │ │ ║
|
||||
║ │ │Component │ │Interfaces │ │ Objects/ │ │ValidationException│ │ ║
|
||||
║ │ └───────────┘ └───────────┘ └───────────┘ └───────────────────┘ │ ║
|
||||
║ │ │ ║
|
||||
║ │ PERMITIDO: Reglas de negocio puras, validaciones, interfaces │ ║
|
||||
║ │ PROHIBIDO: WordPress, HTML, CSS, JS, $wpdb, echo, print │ ║
|
||||
║ │ │ ║
|
||||
║ └─────────────────────────────────────────────────────────────────────┘ ║
|
||||
║ ║
|
||||
║ REGLA DE DEPENDENCIA: Las flechas SOLO apuntan hacia adentro ║
|
||||
║ Infrastructure → Application → Domain ║
|
||||
║ NUNCA al reves: Domain NO depende de nada externo ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estructura Completa de Carpetas del Tema
|
||||
|
||||
```
|
||||
roi-theme/
|
||||
│
|
||||
├── functions.php # Bootstrap del tema
|
||||
├── style.css # Metadata del tema
|
||||
│
|
||||
├── Schemas/ # JSON schemas de componentes
|
||||
│ ├── contact-form.json
|
||||
│ ├── featured-image.json
|
||||
│ ├── footer.json
|
||||
│ └── ...
|
||||
│
|
||||
├── Admin/ # CONTEXTO: Panel de administracion
|
||||
│ ├── ContactForm/ # Modulo: Configuracion de ContactForm
|
||||
│ │ ├── Domain/ # (opcional si no hay logica especifica)
|
||||
│ │ ├── Application/ # (opcional si no hay use cases)
|
||||
│ │ └── Infrastructure/
|
||||
│ │ └── Ui/
|
||||
│ │ └── ContactFormFormBuilder.php
|
||||
│ │
|
||||
│ ├── FeaturedImage/
|
||||
│ │ └── Infrastructure/
|
||||
│ │ └── Ui/
|
||||
│ │ └── FeaturedImageFormBuilder.php
|
||||
│ │
|
||||
│ ├── Shared/ # Compartido SOLO dentro de Admin
|
||||
│ │ └── Infrastructure/
|
||||
│ │ └── Ui/
|
||||
│ │ ├── AdminDashboardRenderer.php
|
||||
│ │ └── ExclusionFormPartial.php
|
||||
│ │
|
||||
│ └── ... # Otros modulos Admin
|
||||
│
|
||||
├── Public/ # CONTEXTO: Frontend publico
|
||||
│ ├── ContactForm/ # Modulo: Renderizado de ContactForm
|
||||
│ │ ├── Domain/ # (opcional)
|
||||
│ │ │ └── Contracts/
|
||||
│ │ ├── Application/ # (opcional)
|
||||
│ │ │ └── UseCases/
|
||||
│ │ └── Infrastructure/
|
||||
│ │ ├── Ui/
|
||||
│ │ │ └── ContactFormRenderer.php
|
||||
│ │ └── Api/
|
||||
│ │ └── WordPress/
|
||||
│ │ └── ContactFormAjaxHandler.php
|
||||
│ │
|
||||
│ ├── FeaturedImage/
|
||||
│ │ └── Infrastructure/
|
||||
│ │ └── Ui/
|
||||
│ │ └── FeaturedImageRenderer.php
|
||||
│ │
|
||||
│ ├── Shared/ # Compartido SOLO dentro de Public
|
||||
│ │ └── Infrastructure/
|
||||
│ │ └── Services/
|
||||
│ │
|
||||
│ └── ... # Otros modulos Public
|
||||
│
|
||||
├── Shared/ # CONTEXTO: Compartido entre Admin Y Public
|
||||
│ ├── Domain/
|
||||
│ │ ├── Contracts/ # Interfaces compartidas
|
||||
│ │ │ ├── RendererInterface.php
|
||||
│ │ │ ├── CSSGeneratorInterface.php
|
||||
│ │ │ ├── ComponentRepositoryInterface.php
|
||||
│ │ │ └── ...
|
||||
│ │ ├── Entities/
|
||||
│ │ │ └── Component.php
|
||||
│ │ └── ValueObjects/
|
||||
│ │
|
||||
│ ├── Application/
|
||||
│ │ └── UseCases/
|
||||
│ │ └── CheckWrapperVisibilityUseCase.php
|
||||
│ │
|
||||
│ └── Infrastructure/
|
||||
│ ├── Services/
|
||||
│ │ ├── CSSGeneratorService.php
|
||||
│ │ └── PageVisibilityHelper.php
|
||||
│ ├── Persistence/
|
||||
│ │ └── WordPress/
|
||||
│ │ ├── ComponentSettingsRepository.php
|
||||
│ │ └── PageVisibilityRepository.php
|
||||
│ └── Scripts/
|
||||
│ └── validate-architecture.php
|
||||
│
|
||||
├── _openspec/ # Sistema de especificaciones
|
||||
│ ├── AGENTS.md
|
||||
│ ├── WORKFLOW-ROI-THEME.md
|
||||
│ ├── project.md
|
||||
│ ├── specs/ # Specs BASE (archivos planos)
|
||||
│ │ ├── arquitectura-limpia.md
|
||||
│ │ ├── estandares-codigo.md
|
||||
│ │ └── nomenclatura.md
|
||||
│ └── changes/ # Specs de features (carpetas)
|
||||
│ └── [nombre-feature]/
|
||||
│
|
||||
└── _planificacion/ # Documentos de planificacion
|
||||
├── 01-design-system/ # Design System del tema
|
||||
├── roi-theme-template/ # Template HTML de referencia
|
||||
├── analisis-spam-formularios.md
|
||||
└── plan-mejora-especificaciones-openspec.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas de Anidamiento
|
||||
|
||||
### Requirement: Profundidad Maxima de Carpetas
|
||||
|
||||
La estructura de carpetas DEBE respetar una profundidad maxima.
|
||||
|
||||
#### Scenario: Profundidad maxima de 4 niveles desde contexto
|
||||
- **WHEN** se crea una estructura de carpetas
|
||||
- **THEN** la profundidad maxima DEBE ser 4 niveles desde el contexto
|
||||
- **AND** ejemplo valido: `Public/ContactForm/Infrastructure/Ui/` (4 niveles)
|
||||
- **AND** ejemplo valido: `Public/ContactForm/Infrastructure/Api/WordPress/` (5 niveles - excepcion para WordPress)
|
||||
- **AND** ejemplo invalido: `Public/ContactForm/Infrastructure/Ui/Partials/Helpers/` (6 niveles)
|
||||
|
||||
#### Scenario: Regla de 3 archivos para subcarpetas
|
||||
- **WHEN** se decide crear una subcarpeta
|
||||
- **THEN** DEBE haber al menos 3 archivos que la justifiquen
|
||||
- **AND** si hay menos de 3 archivos, mantenerlos en la carpeta padre
|
||||
- **AND** ejemplo: NO crear `Ui/Helpers/` con solo 1-2 archivos
|
||||
|
||||
#### Scenario: Subcarpetas permitidas en Infrastructure
|
||||
- **WHEN** se organizan archivos dentro de Infrastructure/
|
||||
- **THEN** subcarpetas permitidas son:
|
||||
- `Ui/` - Renderers, FormBuilders, presentacion
|
||||
- `Api/` - Handlers AJAX, REST endpoints
|
||||
- `Api/WordPress/` - Handlers especificos de WordPress
|
||||
- `Persistence/` - Repositorios genericos
|
||||
- `Persistence/WordPress/` - Repositorios WordPress
|
||||
- `Services/` - Servicios de infraestructura
|
||||
- **AND** NO crear subcarpetas adicionales sin justificacion
|
||||
|
||||
---
|
||||
|
||||
## Diferencia Entre Niveles de Shared
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ NIVELES DE CODIGO COMPARTIDO │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ NIVEL 1: Shared/ (raiz) │
|
||||
│ ───────────────────── │
|
||||
│ QUIEN PUEDE USAR: Admin/ y Public/ │
|
||||
│ CONTENIDO: Contratos base, entidades core, servicios fundamentales │
|
||||
│ EJEMPLOS: │
|
||||
│ - Shared/Domain/Contracts/RendererInterface.php │
|
||||
│ - Shared/Domain/Contracts/CSSGeneratorInterface.php │
|
||||
│ - Shared/Domain/Entities/Component.php │
|
||||
│ - Shared/Infrastructure/Services/CSSGeneratorService.php │
|
||||
│ │
|
||||
│ NIVEL 2: Admin/Shared/ │
|
||||
│ ────────────────────── │
|
||||
│ QUIEN PUEDE USAR: SOLO modulos dentro de Admin/ │
|
||||
│ CONTENIDO: UI components admin, helpers de formularios, partials │
|
||||
│ EJEMPLOS: │
|
||||
│ - Admin/Shared/Infrastructure/Ui/AdminDashboardRenderer.php │
|
||||
│ - Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php │
|
||||
│ PROHIBIDO PARA: Public/ │
|
||||
│ │
|
||||
│ NIVEL 3: Public/Shared/ │
|
||||
│ ─────────────────────── │
|
||||
│ QUIEN PUEDE USAR: SOLO modulos dentro de Public/ │
|
||||
│ CONTENIDO: Helpers de renderizado, componentes frontend compartidos │
|
||||
│ EJEMPLOS: │
|
||||
│ - Public/Shared/Infrastructure/Services/RenderHelper.php │
|
||||
│ PROHIBIDO PARA: Admin/ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
REGLA: Siempre colocar codigo en el nivel MAS ESPECIFICO posible.
|
||||
Solo subir a Shared/ raiz si AMBOS contextos lo necesitan.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ejemplos de Codigo PHP Por Capa
|
||||
|
||||
### Ejemplo CORRECTO: Capa Domain (Interface)
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* Interface para renderizadores de componentes.
|
||||
*
|
||||
* NOTA: Esta interface esta en Domain porque define el CONTRATO
|
||||
* que deben cumplir los renderizadores, sin detalles de implementacion.
|
||||
*/
|
||||
interface RendererInterface
|
||||
{
|
||||
/**
|
||||
* Renderiza un componente y retorna HTML.
|
||||
*/
|
||||
public function render(Component $component): string;
|
||||
|
||||
/**
|
||||
* Verifica si este renderer soporta el tipo de componente.
|
||||
*/
|
||||
public function supports(string $componentType): bool;
|
||||
}
|
||||
```
|
||||
|
||||
### Ejemplo INCORRECTO: Capa Domain (Interface con WordPress)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// ❌ INCORRECTO: WordPress en Domain
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
interface RendererInterface
|
||||
{
|
||||
// ❌ INCORRECTO: Dependencia de WordPress (WP_Post)
|
||||
public function render(WP_Post $post): string;
|
||||
|
||||
// ❌ INCORRECTO: HTML en Domain
|
||||
public function getDefaultHtml(): string;
|
||||
}
|
||||
```
|
||||
|
||||
### Ejemplo CORRECTO: Capa Domain (Entity)
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Entities;
|
||||
|
||||
/**
|
||||
* Entidad que representa un componente del tema.
|
||||
* Contiene SOLO logica de negocio pura.
|
||||
*/
|
||||
final class Component
|
||||
{
|
||||
public function __construct(
|
||||
private string $name,
|
||||
private array $data
|
||||
) {}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return ($this->data['visibility']['is_enabled'] ?? false) === true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ejemplo INCORRECTO: Capa Domain (Entity con WordPress)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// ❌ INCORRECTO: WordPress y persistencia en Domain
|
||||
namespace ROITheme\Shared\Domain\Entities;
|
||||
|
||||
final class Component
|
||||
{
|
||||
// ❌ INCORRECTO: Acceso a BD en Domain
|
||||
public function save(): void
|
||||
{
|
||||
global $wpdb; // ❌ PROHIBIDO
|
||||
$wpdb->insert('wp_components', $this->data);
|
||||
}
|
||||
|
||||
// ❌ INCORRECTO: HTML en Domain
|
||||
public function renderHtml(): string
|
||||
{
|
||||
return '<div>' . esc_html($this->name) . '</div>'; // ❌ PROHIBIDO
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ejemplo CORRECTO: Capa Application (UseCase)
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\ContactForm\Application\UseCases;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* Caso de uso: Obtener configuracion de un componente.
|
||||
* Orquesta llamadas a repositorios, NO contiene logica de negocio.
|
||||
*/
|
||||
final class GetComponentSettingsUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private ComponentRepositoryInterface $repository // ✅ Interface, no clase concreta
|
||||
) {}
|
||||
|
||||
public function execute(string $componentName): ?Component
|
||||
{
|
||||
// ✅ Solo orquestacion, delega a repository
|
||||
return $this->repository->findByName($componentName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ejemplo INCORRECTO: Capa Application (UseCase con WordPress)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// ❌ INCORRECTO: WordPress directo en Application
|
||||
namespace ROITheme\Public\ContactForm\Application\UseCases;
|
||||
|
||||
final class GetComponentSettingsUseCase
|
||||
{
|
||||
public function execute(string $componentName): array
|
||||
{
|
||||
// ❌ INCORRECTO: $wpdb directo en Application
|
||||
global $wpdb;
|
||||
return $wpdb->get_row("SELECT * FROM ...");
|
||||
|
||||
// ❌ INCORRECTO: Funciones WordPress en Application
|
||||
return get_option('roi_component_' . $componentName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ejemplo CORRECTO: Capa Infrastructure (Renderer)
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* Renderer para el formulario de contacto.
|
||||
* Implementa RendererInterface de Domain.
|
||||
*/
|
||||
final class ContactFormRenderer implements RendererInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'contact-form';
|
||||
|
||||
public function __construct(
|
||||
private CSSGeneratorInterface $cssGenerator // ✅ DI via interface
|
||||
) {}
|
||||
|
||||
public function render(Component $component): string
|
||||
{
|
||||
$data = $component->getData();
|
||||
|
||||
// ✅ WordPress permitido en Infrastructure
|
||||
$nonce = wp_create_nonce('roi_contact_form');
|
||||
|
||||
// ✅ HTML permitido en Infrastructure
|
||||
$html = '<form id="roiContactForm" data-nonce="' . esc_attr($nonce) . '">';
|
||||
// ... mas HTML
|
||||
$html .= '</form>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === self::COMPONENT_NAME;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ejemplo CORRECTO: Capa Infrastructure (Repository)
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\ComponentRepositoryInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
|
||||
/**
|
||||
* Repository WordPress para componentes.
|
||||
* Implementa interface de Domain usando $wpdb.
|
||||
*/
|
||||
final class ComponentSettingsRepository implements ComponentRepositoryInterface
|
||||
{
|
||||
public function findByName(string $componentName): ?Component
|
||||
{
|
||||
global $wpdb; // ✅ WordPress permitido en Infrastructure
|
||||
|
||||
$table = $wpdb->prefix . 'roi_theme_component_settings';
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE component_name = %s",
|
||||
$componentName
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if (empty($results)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Component($componentName, $this->groupResults($results));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mapeo de Terminologia
|
||||
|
||||
| Clean Architecture Estandar | ROI Theme | Ubicacion |
|
||||
|---------------------------|-----------|-----------|
|
||||
| Entity | Component, Entity | `*/Domain/Entities/` |
|
||||
| Value Object | Value Object | `*/Domain/ValueObjects/` |
|
||||
| Repository Interface | RepositoryInterface | `Shared/Domain/Contracts/` |
|
||||
| Repository Implementation | Repository | `*/Infrastructure/Persistence/WordPress/` |
|
||||
| Use Case / Interactor | UseCase | `*/Application/UseCases/` |
|
||||
| Gateway | Repository | `*/Infrastructure/Persistence/` |
|
||||
| Presenter | Renderer | `Public/*/Infrastructure/Ui/` |
|
||||
| Controller | FormBuilder, Handler | `Admin/*/Infrastructure/Ui/`, `*/Infrastructure/Api/` |
|
||||
| DTO | Request/Response | `*/Application/DTOs/` |
|
||||
| Domain Service | Service | `*/Domain/Services/` |
|
||||
| Infrastructure Service | Service | `*/Infrastructure/Services/` |
|
||||
|
||||
---
|
||||
|
||||
## Validacion de Arquitectura
|
||||
|
||||
### Script de Validacion
|
||||
|
||||
Ubicacion: `Shared/Infrastructure/Scripts/validate-architecture.php`
|
||||
|
||||
```bash
|
||||
# Validar un componente especifico
|
||||
php Shared/Infrastructure/Scripts/validate-architecture.php contact-form
|
||||
|
||||
# Validar todos los componentes
|
||||
php Shared/Infrastructure/Scripts/validate-architecture.php --all
|
||||
```
|
||||
|
||||
### Que Valida el Script
|
||||
|
||||
| Validacion | Que Busca | Error si Encuentra |
|
||||
|------------|-----------|-------------------|
|
||||
| WordPress en Domain | `global $wpdb`, `add_action`, `$_POST` | "WordPress code in Domain layer" |
|
||||
| HTML en Domain | `<div`, `<form`, `echo` | "HTML/Output in Domain layer" |
|
||||
| Imports cruzados | `Admin/X` importando de `Admin/Y` | "Cross-module import detected" |
|
||||
| Direccion dependencias | Application importando Infrastructure | "Invalid dependency direction" |
|
||||
| Nomenclatura | Carpetas no-PascalCase | "Invalid folder naming" |
|
||||
| Interface implementation | Renderer sin RendererInterface | "Missing interface implementation" |
|
||||
| Strict types | Archivo sin `declare(strict_types=1)` | "Missing strict_types declaration" |
|
||||
|
||||
### Checklist Pre-Commit de Arquitectura
|
||||
|
||||
```
|
||||
[ ] Archivos Domain NO contienen: $wpdb, add_action, $_POST, echo, HTML
|
||||
[ ] Archivos Application NO contienen: $wpdb, HTML, WordPress functions
|
||||
[ ] Clases Infrastructure implementan interfaces de Domain
|
||||
[ ] No hay imports cruzados entre modulos del mismo contexto
|
||||
[ ] Carpetas siguen nomenclatura PascalCase
|
||||
[ ] Archivos PHP tienen declare(strict_types=1)
|
||||
[ ] DI es via constructor con interfaces
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2026-01-08
|
||||
1235
_openspec/specs/estandares-codigo.md
Normal file
1235
_openspec/specs/estandares-codigo.md
Normal file
File diff suppressed because it is too large
Load Diff
687
_openspec/specs/nomenclatura.md
Normal file
687
_openspec/specs/nomenclatura.md
Normal file
@@ -0,0 +1,687 @@
|
||||
# Especificacion de Nomenclatura - ROI Theme
|
||||
|
||||
## Purpose
|
||||
|
||||
Define las convenciones de nomenclatura para carpetas, archivos, clases, metodos, propiedades y variables en el tema ROI Theme (PHP 8.x / WordPress).
|
||||
|
||||
> **NOTA**: Para principios SOLID y estandares de codigo, ver `_openspec/specs/estandares-codigo.md`
|
||||
> **NOTA**: Para arquitectura y modularidad, ver `_openspec/specs/arquitectura-limpia.md`
|
||||
|
||||
---
|
||||
|
||||
## Resumen de Nomenclaturas
|
||||
|
||||
| Elemento | Nomenclatura | Ejemplo |
|
||||
|----------|-------------|---------|
|
||||
| **Carpetas principales** | PascalCase | `Admin/`, `Public/`, `Shared/` |
|
||||
| **Carpetas de contexto** | PascalCase | `Admin/`, `Public/` |
|
||||
| **Carpetas de modulo** | PascalCase | `ContactForm/`, `FeaturedImage/` |
|
||||
| **Carpetas de capa** | PascalCase | `Domain/`, `Application/`, `Infrastructure/` |
|
||||
| **Carpetas auxiliares** | _minusculas | `_planificacion/`, `_arquitectura/` |
|
||||
| **Archivos PHP de clase** | PascalCase.php | `ContactFormRenderer.php` |
|
||||
| **Archivos PHP de interface** | PascalCaseInterface.php | `RendererInterface.php` |
|
||||
| **Archivos JSON schema** | kebab-case.json | `contact-form.json` |
|
||||
| **Namespaces** | PascalCase | `ROITheme\Public\ContactForm\Infrastructure\Ui` |
|
||||
| **Clases** | PascalCase | `ContactFormRenderer` |
|
||||
| **Interfaces** | PascalCase + Interface | `RendererInterface`, `CSSGeneratorInterface` |
|
||||
| **Metodos** | camelCase | `render()`, `getVisibilityClass()` |
|
||||
| **Propiedades** | camelCase | `$cssGenerator`, `$componentName` |
|
||||
| **Variables locales** | $camelCase | `$showDesktop`, `$visibilityClass` |
|
||||
| **Parametros** | $camelCase | `$component`, `$data` |
|
||||
| **Campos privados** | $camelCase | `$cssGenerator` (con private) |
|
||||
| **Constantes clase** | UPPER_SNAKE_CASE | `COMPONENT_NAME`, `MAX_ITEMS` |
|
||||
| **Constantes globales** | UPPER_SNAKE_CASE | `ROI_THEME_VERSION` |
|
||||
| **component_name** | kebab-case | `"contact-form"`, `"featured-image"` |
|
||||
| **Hooks WordPress** | snake_case con prefijo | `roi_theme_after_render` |
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Nomenclatura de Carpetas
|
||||
|
||||
Los nombres de carpetas siguen convenciones basadas en su proposito.
|
||||
|
||||
#### Scenario: Carpetas principales del tema
|
||||
- **WHEN** se nombra una carpeta principal de codigo
|
||||
- **THEN** DEBE usar PascalCase
|
||||
- **AND** ejemplos correctos: `Admin/`, `Public/`, `Shared/`, `Schemas/`
|
||||
- **AND** ejemplos incorrectos: `admin/`, `ADMIN/`, `shared_code/`
|
||||
|
||||
#### Scenario: Carpetas de contexto
|
||||
- **WHEN** se nombra una carpeta que representa un contexto de la aplicacion
|
||||
- **THEN** DEBE usar PascalCase
|
||||
- **AND** carpetas permitidas:
|
||||
- `Admin/` - Componentes del panel de administracion
|
||||
- `Public/` - Componentes del frontend publico
|
||||
- `Shared/` - Codigo compartido entre contextos
|
||||
- **AND** ejemplos incorrectos: `backend/`, `frontend/`, `common/`
|
||||
|
||||
#### Scenario: Carpetas de modulo (componente)
|
||||
- **WHEN** se nombra una carpeta que representa un modulo/componente
|
||||
- **THEN** DEBE seguir PascalCase
|
||||
- **AND** DEBE coincidir con component_name convertido de kebab-case
|
||||
- **AND** ejemplos correctos:
|
||||
- `ContactForm/` (de `contact-form`)
|
||||
- `FeaturedImage/` (de `featured-image`)
|
||||
- `TopNotificationBar/` (de `top-notification-bar`)
|
||||
- `CtaBoxSidebar/` (de `cta-box-sidebar`)
|
||||
- **AND** ejemplos incorrectos:
|
||||
- `contact-form/` (kebab-case)
|
||||
- `contactForm/` (camelCase)
|
||||
- `CONTACT_FORM/` (UPPER_SNAKE)
|
||||
|
||||
#### Scenario: Carpetas de capa (Clean Architecture)
|
||||
- **WHEN** se nombra una carpeta que representa una capa de arquitectura
|
||||
- **THEN** DEBE usar PascalCase
|
||||
- **AND** carpetas permitidas dentro de cada modulo:
|
||||
- `Domain/` - Entidades, interfaces, value objects
|
||||
- `Application/` - Casos de uso
|
||||
- `Infrastructure/` - Implementaciones (Ui, Api, Persistence, Services)
|
||||
- **AND** subcarpetas de Infrastructure permitidas:
|
||||
- `Ui/` - Renderers, FormBuilders
|
||||
- `Api/` - Handlers AJAX, REST endpoints
|
||||
- `Persistence/` - Repositorios
|
||||
- `Services/` - Servicios de infraestructura
|
||||
- `WordPress/` - Integraciones especificas de WordPress
|
||||
- **AND** ejemplos correctos: `Infrastructure/Ui/`, `Infrastructure/Api/WordPress/`
|
||||
- **AND** ejemplos incorrectos: `infrastructure/`, `UI/`, `api/`
|
||||
|
||||
#### Scenario: Carpetas auxiliares (no codigo)
|
||||
- **WHEN** se nombra una carpeta de documentacion, planificacion o configuracion
|
||||
- **THEN** DEBE usar prefijo guion bajo + minusculas
|
||||
- **AND** ejemplos correctos:
|
||||
- `_planificacion/` - Documentos de planificacion
|
||||
- `_arquitectura/` - Documentos de arquitectura
|
||||
- **AND** nombres compuestos usan guion medio: `_pruebas-regresion/`
|
||||
- **AND** el guion bajo indica carpetas auxiliares/no-codigo
|
||||
|
||||
#### Scenario: Carpetas de cambios (OpenSpec)
|
||||
- **WHEN** se crea una carpeta para un cambio/feature en `_openspec/changes/`
|
||||
- **THEN** DEBE usar nombre en kebab-case descriptivo
|
||||
- **AND** maximo 1 nivel de carpeta despues de `changes/`
|
||||
- **AND** ejemplos correctos:
|
||||
- `anti-spam-validator/`
|
||||
- `lazy-loading-images/`
|
||||
- `improved-caching/`
|
||||
- **AND** ejemplos incorrectos:
|
||||
- `AntiSpam/` (PascalCase)
|
||||
- `anti_spam/` (snake_case)
|
||||
- `anti-spam/subfolder/` (anidamiento prohibido)
|
||||
|
||||
#### Scenario: Excepciones permitidas
|
||||
- **WHEN** existen carpetas especiales del sistema
|
||||
- **THEN** se permiten las siguientes excepciones:
|
||||
- `.git/` - Control de versiones
|
||||
- `.serena/` - Configuracion de Serena MCP
|
||||
- `.claude/` - Configuracion de Claude Code
|
||||
- `node_modules/` - Dependencias npm (si aplica)
|
||||
- `vendor/` - Dependencias Composer (si aplica)
|
||||
- `_openspec/` - Sistema de especificaciones (carpeta auxiliar)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de Archivos PHP
|
||||
|
||||
Los archivos PHP DEBEN seguir convencion PascalCase.
|
||||
|
||||
#### Scenario: Archivos de clase
|
||||
- **WHEN** se nombra un archivo que contiene una clase PHP
|
||||
- **THEN** DEBE seguir PascalCase
|
||||
- **AND** el nombre DEBE coincidir EXACTAMENTE con el nombre de la clase
|
||||
- **AND** extension `.php`
|
||||
- **AND** ejemplos correctos:
|
||||
- `ContactFormRenderer.php` (contiene `class ContactFormRenderer`)
|
||||
- `NewsletterAjaxHandler.php` (contiene `class NewsletterAjaxHandler`)
|
||||
- `CSSGeneratorService.php` (contiene `class CSSGeneratorService`)
|
||||
- **AND** ejemplos incorrectos:
|
||||
- `contact-form-renderer.php` (kebab-case)
|
||||
- `contactFormRenderer.php` (camelCase)
|
||||
- `class_contact_form.php` (snake_case con prefijo)
|
||||
|
||||
#### Scenario: Archivos de interface
|
||||
- **WHEN** se nombra un archivo que contiene una interface
|
||||
- **THEN** DEBE seguir PascalCase con sufijo Interface
|
||||
- **AND** extension `.php`
|
||||
- **AND** ejemplos correctos:
|
||||
- `RendererInterface.php`
|
||||
- `CSSGeneratorInterface.php`
|
||||
- `ComponentRepositoryInterface.php`
|
||||
- **AND** ejemplos incorrectos:
|
||||
- `IRenderer.php` (prefijo I estilo C#)
|
||||
- `Renderer.php` (sin sufijo)
|
||||
|
||||
#### Scenario: Archivos de trait
|
||||
- **WHEN** se nombra un archivo que contiene un trait PHP
|
||||
- **THEN** DEBE seguir PascalCase con sufijo Trait
|
||||
- **AND** extension `.php`
|
||||
- **AND** ejemplos correctos:
|
||||
- `VisibilityTrait.php`
|
||||
- `CSSGeneratorTrait.php`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de Archivos JSON (Schemas)
|
||||
|
||||
Los schemas JSON DEBEN usar kebab-case.
|
||||
|
||||
#### Scenario: Archivos de schema de componente
|
||||
- **WHEN** se nombra un archivo JSON schema
|
||||
- **THEN** DEBE usar kebab-case
|
||||
- **AND** extension `.json`
|
||||
- **AND** el nombre DEBE coincidir con el component_name
|
||||
- **AND** ejemplos correctos:
|
||||
- `contact-form.json` (component_name: "contact-form")
|
||||
- `featured-image.json` (component_name: "featured-image")
|
||||
- `top-notification-bar.json` (component_name: "top-notification-bar")
|
||||
- `cta-box-sidebar.json` (component_name: "cta-box-sidebar")
|
||||
- **AND** ejemplos incorrectos:
|
||||
- `ContactForm.json` (PascalCase)
|
||||
- `contact_form.json` (snake_case)
|
||||
- `contactForm.json` (camelCase)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de Namespaces
|
||||
|
||||
Los namespaces PHP DEBEN seguir convencion PascalCase jerarquica.
|
||||
|
||||
#### Scenario: Namespace raiz
|
||||
- **WHEN** se define el namespace principal del tema
|
||||
- **THEN** DEBE ser `ROITheme`
|
||||
- **AND** ejemplo: `namespace ROITheme;`
|
||||
|
||||
#### Scenario: Namespaces de modulo publico
|
||||
- **WHEN** se define un namespace para un componente en Public/
|
||||
- **THEN** DEBE seguir `ROITheme\Public\[Componente]\[Capa][\Subcapa]`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
// Renderers
|
||||
namespace ROITheme\Public\ContactForm\Infrastructure\Ui;
|
||||
namespace ROITheme\Public\FeaturedImage\Infrastructure\Ui;
|
||||
namespace ROITheme\Public\TopNotificationBar\Infrastructure\Ui;
|
||||
|
||||
// AJAX Handlers
|
||||
namespace ROITheme\Public\Footer\Infrastructure\Api\WordPress;
|
||||
namespace ROITheme\Public\ContactForm\Infrastructure\Api\WordPress;
|
||||
|
||||
// Domain (si existe)
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\ValueObjects;
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\Contracts;
|
||||
|
||||
// Application (si existe)
|
||||
namespace ROITheme\Public\CustomCSSManager\Application\UseCases;
|
||||
```
|
||||
|
||||
#### Scenario: Namespaces de modulo admin
|
||||
- **WHEN** se define un namespace para un componente en Admin/
|
||||
- **THEN** DEBE seguir `ROITheme\Admin\[Componente]\[Capa][\Subcapa]`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
// FormBuilders
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
|
||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
|
||||
|
||||
// Shared de Admin
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Ui;
|
||||
```
|
||||
|
||||
#### Scenario: Namespaces de Shared
|
||||
- **WHEN** se define un namespace para codigo compartido
|
||||
- **THEN** DEBE seguir `ROITheme\Shared\[Capa][\Subcapa]`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
// Domain
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
namespace ROITheme\Shared\Domain\Entities;
|
||||
namespace ROITheme\Shared\Domain\ValueObjects;
|
||||
|
||||
// Application
|
||||
namespace ROITheme\Shared\Application\UseCases;
|
||||
|
||||
// Infrastructure
|
||||
namespace ROITheme\Shared\Infrastructure\Services;
|
||||
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de Clases
|
||||
|
||||
Los nombres de clase DEBEN seguir convencion PascalCase.
|
||||
|
||||
#### Scenario: Clases regulares
|
||||
- **WHEN** se define una clase en PHP
|
||||
- **THEN** el nombre DEBE seguir PascalCase
|
||||
- **AND** DEBE ser un sustantivo o frase sustantiva
|
||||
- **AND** DEBE ser descriptivo de su responsabilidad
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
final class ContactFormRenderer
|
||||
final class ComponentSettings
|
||||
final class VisibilityChecker
|
||||
```
|
||||
- **AND** ejemplos incorrectos:
|
||||
```php
|
||||
final class contactFormRenderer // camelCase
|
||||
final class contact_form_renderer // snake_case
|
||||
final class Render // verbo, no sustantivo
|
||||
```
|
||||
|
||||
#### Scenario: Clases Renderer
|
||||
- **WHEN** se define una clase que renderiza HTML de un componente
|
||||
- **THEN** DEBE usar sufijo `Renderer`
|
||||
- **AND** DEBE implementar `RendererInterface`
|
||||
- **AND** DEBE ser `final`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
final class ContactFormRenderer implements RendererInterface
|
||||
final class FeaturedImageRenderer implements RendererInterface
|
||||
final class TopNotificationBarRenderer implements RendererInterface
|
||||
```
|
||||
|
||||
#### Scenario: Clases FormBuilder
|
||||
- **WHEN** se define una clase que genera formularios admin
|
||||
- **THEN** DEBE usar sufijo `FormBuilder`
|
||||
- **AND** DEBE ser `final`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
final class ContactFormFormBuilder
|
||||
final class FeaturedImageFormBuilder
|
||||
final class TopNotificationBarFormBuilder
|
||||
```
|
||||
|
||||
#### Scenario: Clases de caso de uso
|
||||
- **WHEN** se define una clase UseCase en Application/
|
||||
- **THEN** DEBE usar sufijo `UseCase`
|
||||
- **AND** el nombre DEBE describir la accion
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
final class GetCriticalCSSUseCase
|
||||
final class CheckAdsenseVisibilityUseCase
|
||||
final class GetDeferredSnippetsUseCase
|
||||
```
|
||||
|
||||
#### Scenario: Clases de servicio
|
||||
- **WHEN** se define una clase que provee servicios en Infrastructure/
|
||||
- **THEN** DEBE usar sufijo `Service`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
final class CSSGeneratorService
|
||||
final class AntiSpamValidatorService
|
||||
final class CacheService
|
||||
```
|
||||
|
||||
#### Scenario: Clases de repositorio
|
||||
- **WHEN** se define una clase de acceso a datos
|
||||
- **THEN** DEBE usar sufijo `Repository`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
final class ComponentSettingsRepository
|
||||
final class PageVisibilityRepository
|
||||
```
|
||||
|
||||
#### Scenario: Clases Handler (AJAX/API)
|
||||
- **WHEN** se define una clase que maneja peticiones AJAX o API
|
||||
- **THEN** DEBE usar sufijo `Handler` o `Controller`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
final class NewsletterAjaxHandler
|
||||
final class ContactFormAjaxHandler
|
||||
final class AdsenseVisibilityController
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de Interfaces
|
||||
|
||||
Los nombres de interface DEBEN seguir convencion PascalCase con sufijo.
|
||||
|
||||
#### Scenario: Interfaces
|
||||
- **WHEN** se define una interface en PHP
|
||||
- **THEN** el nombre DEBE terminar con `Interface`
|
||||
- **AND** DEBE seguir PascalCase
|
||||
- **AND** DEBE describir la capacidad o contrato
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
interface RendererInterface
|
||||
interface CSSGeneratorInterface
|
||||
interface ComponentRepositoryInterface
|
||||
interface AjaxControllerInterface
|
||||
```
|
||||
- **AND** ejemplos incorrectos:
|
||||
```php
|
||||
interface IRenderer // prefijo I estilo C#
|
||||
interface Renderer // sin sufijo
|
||||
interface renderer_interface // snake_case
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de Metodos
|
||||
|
||||
Los nombres de metodo DEBEN seguir convencion camelCase.
|
||||
|
||||
#### Scenario: Metodos publicos
|
||||
- **WHEN** se define un metodo publico
|
||||
- **THEN** el nombre DEBE seguir camelCase
|
||||
- **AND** DEBE comenzar con verbo que describe la accion
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
public function render(Component $component): string
|
||||
public function getVisibilityClass(array $data): ?string
|
||||
public function validateInput(string $input): bool
|
||||
public function buildForm(string $componentId): string
|
||||
```
|
||||
- **AND** ejemplos incorrectos:
|
||||
```php
|
||||
public function Render() // PascalCase
|
||||
public function get_visibility_class() // snake_case
|
||||
public function visibility() // sin verbo
|
||||
```
|
||||
|
||||
#### Scenario: Metodos privados
|
||||
- **WHEN** se define un metodo privado
|
||||
- **THEN** DEBE seguir camelCase (igual que publicos)
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
private function parseResponse(): array
|
||||
private function validateInternal(): bool
|
||||
private function generateCSS(array $data): string
|
||||
```
|
||||
|
||||
#### Scenario: Metodos booleanos
|
||||
- **WHEN** un metodo retorna un valor booleano
|
||||
- **THEN** DEBE usar prefijo `is`, `has`, `can`, `should`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
public function isEnabled(array $data): bool
|
||||
public function hasPermission(string $capability): bool
|
||||
public function canProcess(): bool
|
||||
public function shouldShow(string $component): bool
|
||||
```
|
||||
- **AND** ejemplos incorrectos:
|
||||
```php
|
||||
public function enabled(): bool // sin prefijo
|
||||
public function checkEnabled(): bool // check no es booleano
|
||||
public function getIsEnabled(): bool // get redundante
|
||||
```
|
||||
|
||||
#### Scenario: Metodos getter/setter
|
||||
- **WHEN** se define un metodo de acceso
|
||||
- **THEN** getters DEBEN usar prefijo `get`
|
||||
- **AND** setters DEBEN usar prefijo `set`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
public function getData(): array
|
||||
public function setData(array $data): void
|
||||
public function getComponentName(): string
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de Propiedades y Variables
|
||||
|
||||
Las propiedades y variables DEBEN seguir convencion camelCase.
|
||||
|
||||
#### Scenario: Propiedades de clase
|
||||
- **WHEN** se declara una propiedad de clase
|
||||
- **THEN** DEBE seguir camelCase
|
||||
- **AND** DEBE tener visibilidad explicita (private, protected, public)
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
private CSSGeneratorInterface $cssGenerator;
|
||||
private string $componentName;
|
||||
protected array $settings;
|
||||
```
|
||||
- **AND** ejemplos incorrectos:
|
||||
```php
|
||||
private $CssGenerator; // PascalCase
|
||||
private $css_generator; // snake_case
|
||||
private $_cssGenerator; // prefijo _ (no necesario en PHP moderno)
|
||||
```
|
||||
|
||||
#### Scenario: Propiedades booleanas
|
||||
- **WHEN** se define una propiedad booleana
|
||||
- **THEN** DEBE usar prefijo `is`, `has`, `can`
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
private bool $isEnabled;
|
||||
private bool $hasChanges;
|
||||
private bool $canEdit;
|
||||
```
|
||||
|
||||
#### Scenario: Variables locales
|
||||
- **WHEN** se declara una variable local
|
||||
- **THEN** DEBE seguir $camelCase
|
||||
- **AND** DEBE ser descriptiva
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
$showDesktop = $data['visibility']['show_on_desktop'] ?? true;
|
||||
$visibilityClass = $this->getVisibilityClass($data);
|
||||
$componentSettings = $this->repository->get($componentName);
|
||||
```
|
||||
- **AND** ejemplos incorrectos:
|
||||
```php
|
||||
$ShowDesktop // PascalCase
|
||||
$show_desktop // snake_case
|
||||
$sd // abreviatura
|
||||
$strShowDesktop // notacion hungara
|
||||
```
|
||||
|
||||
#### Scenario: Parametros de metodo
|
||||
- **WHEN** se declara un parametro de metodo
|
||||
- **THEN** DEBE seguir $camelCase
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
public function render(Component $component): string
|
||||
public function validateField(string $fieldName, mixed $value): bool
|
||||
```
|
||||
|
||||
#### Scenario: Variables de iteracion
|
||||
- **WHEN** se usa una variable de iteracion en bucle
|
||||
- **THEN** se permiten nombres cortos para indices: `$i`, `$j`, `$k`
|
||||
- **AND** se prefiere nombre descriptivo para elementos
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
for ($i = 0; $i < count($items); $i++)
|
||||
foreach ($components as $component)
|
||||
foreach ($settings as $key => $value)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de Constantes
|
||||
|
||||
Las constantes DEBEN seguir convencion UPPER_SNAKE_CASE.
|
||||
|
||||
#### Scenario: Constantes de clase
|
||||
- **WHEN** se define una constante de clase
|
||||
- **THEN** DEBE seguir UPPER_SNAKE_CASE
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
private const COMPONENT_NAME = 'contact-form';
|
||||
public const MAX_RETRY_COUNT = 3;
|
||||
protected const DEFAULT_TIMEOUT = 5000;
|
||||
```
|
||||
- **AND** ejemplos incorrectos:
|
||||
```php
|
||||
private const componentName = 'contact-form'; // camelCase
|
||||
private const ComponentName = 'contact-form'; // PascalCase
|
||||
```
|
||||
|
||||
#### Scenario: Constantes globales
|
||||
- **WHEN** se define una constante global del tema
|
||||
- **THEN** DEBE usar prefijo `ROI_THEME_`
|
||||
- **AND** DEBE seguir UPPER_SNAKE_CASE
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
define('ROI_THEME_VERSION', '1.0.0');
|
||||
define('ROI_THEME_PATH', get_template_directory());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de component_name
|
||||
|
||||
El identificador de componente DEBE usar kebab-case.
|
||||
|
||||
#### Scenario: component_name en JSON
|
||||
- **WHEN** se define component_name en un schema JSON
|
||||
- **THEN** DEBE usar kebab-case
|
||||
- **AND** DEBE coincidir con el nombre del archivo JSON
|
||||
- **AND** ejemplos correctos:
|
||||
```json
|
||||
{
|
||||
"component_name": "contact-form",
|
||||
"component_name": "featured-image",
|
||||
"component_name": "top-notification-bar",
|
||||
"component_name": "cta-box-sidebar"
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: component_name en BD
|
||||
- **WHEN** se guarda component_name en base de datos
|
||||
- **THEN** DEBE mantener kebab-case
|
||||
- **AND** tabla: `wp_roi_theme_component_settings`
|
||||
- **AND** columna: `component_name`
|
||||
|
||||
#### Scenario: component_name en codigo PHP
|
||||
- **WHEN** se usa component_name en codigo PHP
|
||||
- **THEN** DEBE mantenerse en kebab-case
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
private const COMPONENT_NAME = 'contact-form';
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'contact-form';
|
||||
}
|
||||
|
||||
// En data-attribute
|
||||
$html .= 'data-component="contact-form"';
|
||||
```
|
||||
|
||||
#### Scenario: Conversion kebab-case a PascalCase
|
||||
- **WHEN** se necesita convertir component_name a nombre de carpeta/clase
|
||||
- **THEN** eliminar guiones y capitalizar cada palabra
|
||||
- **AND** ejemplos de conversion:
|
||||
| kebab-case | PascalCase |
|
||||
|------------|------------|
|
||||
| `contact-form` | `ContactForm` |
|
||||
| `featured-image` | `FeaturedImage` |
|
||||
| `top-notification-bar` | `TopNotificationBar` |
|
||||
| `cta-box-sidebar` | `CtaBoxSidebar` |
|
||||
| `cta-lets-talk` | `CtaLetsTalk` |
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Nomenclatura de Hooks WordPress
|
||||
|
||||
Los hooks DEBEN usar snake_case con prefijo del tema.
|
||||
|
||||
#### Scenario: Actions del tema
|
||||
- **WHEN** se define un action hook del tema
|
||||
- **THEN** DEBE usar prefijo `roi_theme_`
|
||||
- **AND** DEBE seguir snake_case
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
do_action('roi_theme_after_render', $component);
|
||||
do_action('roi_theme_before_form_submit');
|
||||
do_action('roi_theme_component_loaded', $componentName);
|
||||
```
|
||||
|
||||
#### Scenario: Filters del tema
|
||||
- **WHEN** se define un filter hook del tema
|
||||
- **THEN** DEBE usar prefijo `roi_theme_filter_`
|
||||
- **AND** DEBE seguir snake_case
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
$css = apply_filters('roi_theme_filter_component_css', $css, $component);
|
||||
$html = apply_filters('roi_theme_filter_render_output', $html);
|
||||
```
|
||||
|
||||
#### Scenario: Acciones AJAX
|
||||
- **WHEN** se registra una accion AJAX
|
||||
- **THEN** DEBE usar prefijo `roi_`
|
||||
- **AND** DEBE seguir snake_case
|
||||
- **AND** ejemplos correctos:
|
||||
```php
|
||||
add_action('wp_ajax_roi_newsletter_subscribe', [$this, 'handle']);
|
||||
add_action('wp_ajax_nopriv_roi_contact_form_submit', [$this, 'handle']);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Prohibicion de Notacion Hungara
|
||||
|
||||
La notacion hungara esta PROHIBIDA.
|
||||
|
||||
#### Scenario: Prefijos de tipo prohibidos
|
||||
- **WHEN** se nombra una variable, propiedad o parametro
|
||||
- **THEN** NO DEBE usar prefijos de tipo
|
||||
- **AND** prefijos prohibidos:
|
||||
|
||||
| Prefijo | Significado | Ejemplo Incorrecto |
|
||||
|---------|-------------|-------------------|
|
||||
| `str` | String | `$strNombre` |
|
||||
| `int`, `i` | Integer | `$intContador`, `$iTotal` |
|
||||
| `b`, `bln` | Boolean | `$bActivo`, `$blnValido` |
|
||||
| `arr` | Array | `$arrItems` |
|
||||
| `obj` | Object | `$objRenderer` |
|
||||
|
||||
#### Scenario: Nombres correctos sin notacion hungara
|
||||
- **WHEN** se reemplaza notacion hungara
|
||||
- **THEN** usar nombres descriptivos
|
||||
|
||||
| Incorrecto | Correcto |
|
||||
|------------|----------|
|
||||
| `$strNombre` | `$name` o `$customerName` |
|
||||
| `$bActivo` | `$isActive` |
|
||||
| `$arrItems` | `$items` |
|
||||
| `$objRenderer` | `$renderer` |
|
||||
| `$intCount` | `$count` o `$itemCount` |
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Validacion Pre-Commit de Nomenclatura
|
||||
|
||||
Las convenciones DEBEN validarse antes del commit.
|
||||
|
||||
#### Scenario: Checklist de nomenclatura
|
||||
- **WHEN** el codigo esta listo para commit
|
||||
- **THEN** verificar:
|
||||
- [ ] Carpetas de modulo en PascalCase
|
||||
- [ ] Archivos PHP en PascalCase.php
|
||||
- [ ] Archivos JSON schema en kebab-case.json
|
||||
- [ ] Namespaces en PascalCase jerarquico
|
||||
- [ ] Clases en PascalCase
|
||||
- [ ] Interfaces con sufijo Interface
|
||||
- [ ] Metodos en camelCase
|
||||
- [ ] Propiedades en camelCase
|
||||
- [ ] Variables locales en $camelCase
|
||||
- [ ] Constantes en UPPER_SNAKE_CASE
|
||||
- [ ] component_name en kebab-case
|
||||
- [ ] Hooks con prefijo roi_theme_
|
||||
- [ ] SIN notacion hungara
|
||||
- [ ] Metodos booleanos con is/has/can/should
|
||||
- [ ] Renderers con sufijo Renderer
|
||||
- [ ] FormBuilders con sufijo FormBuilder
|
||||
- [ ] UseCases con sufijo UseCase
|
||||
- [ ] Services con sufijo Service
|
||||
|
||||
---
|
||||
|
||||
## Tabla de Conversion Rapida
|
||||
|
||||
| De | A | Ejemplo |
|
||||
|----|---|---------|
|
||||
| kebab-case | PascalCase | `contact-form` → `ContactForm` |
|
||||
| PascalCase | kebab-case | `ContactForm` → `contact-form` |
|
||||
| PascalCase | camelCase | `ContactForm` → `contactForm` |
|
||||
| snake_case | PascalCase | `contact_form` → `ContactForm` |
|
||||
| snake_case | camelCase | `contact_form` → `contactForm` |
|
||||
|
||||
---
|
||||
|
||||
**Última actualización:** 2026-01-08
|
||||
Reference in New Issue
Block a user