Blog

Lead Nurturing con Email Marketing: Automatiza Conversiones SEO 2026

Lead nurturing con email marketing

El SEO atrae tráfico. El email marketing convierte. Pero entre medias hay un abismo: el 80% de los leads que llegan desde SEO no compran inmediatamente. No están listos. O no confían lo suficiente. O simplemente se olvidan.

  • Pero entre medias hay un abismo: el 80% de los leads que llegan desde SEO no compran inmediatamente.

Aquí entra el lead nurturing: el arte de cultivar leads a través del tiempo con contenido relevante, en el momento justo. Manualmente es imposible de escalar. Automatizado, es una máquina de conversión.

Por qué Lead Nurturing es crítico para SEO

El problema:

  • Invieres en SEO para atraer tráfico
  • El 70-80% no convierte en la primera visita
  • Perdes esos leads en el olvido (o van a la competencia)
  • ROI del SEO se ve afectado: "no funciona, bajo ROI"

La solución:

  • Lead nurturing automatizado: Sequences de email basadas en comportamiento
  • Personalización: Contenido relevante según etapa del funnel
  • Timing inteligente: Emails en el momento justo, no spam
  • Medición continua: Qué funciona, qué no funciona, optimizar

Qué vas a construir

Vamos a crear un sistema completo de lead nurturing que incluye:

  • Integración SEO → Email: Capturar intención de búsqueda automáticamente
  • Segmentación dinámica: Clasificar leads según comportamiento
  • 4 sequences automatizadas: Welcome, Re-engagement, Behavioral, Abandoned Content
  • Lead scoring: Puntuación basada en interacción
  • Métricas y ROI: Dashboard para medir éxito

Fundamentos de Lead Nurturing

Qué es (y qué NO es)

Lead nurturing: Proceso de cultivar relaciones con leads a través del tiempo, entregando valor en cada interacción.

NO es:

  • No es spam masivo (enviar lo mismo a todos)
  • No es email blasts indiscriminados
  • No es newsletter genérico

SÍ es:

  • Contenido hiper-personalizado
  • Timing basado en comportamiento del usuario
  • Progresivo: educa → demuestra → convierte

Buyer Journey en SEO

Los leads de SEO vienen con diferente intención según cómo llegaron:

Diferencia vs Email Marketing Tradicional

Framework Base: Estructura del Sistema

Estructura de proyecto

text
lead-nurturing/
├── .env                    # Variables de entorno
├── requirements.txt        # Dependencias
├── config.py              # Configuración centralizada
├── models/
│   ├── lead.py            # Modelo de datos Lead
│   ├── sequence.py        # Modelo de datos Sequence
│   └── email.py           # Modelo de datos Email
├── services/
│   ├── capture.py         # Capturar leads SEO
│   ├── segment.py          # Segmentación automática
│   ├── score.py            # Lead scoring
│   └── esp.py              # Integración Email API
├── sequences/
│   ├── welcome.py          # Sequence 1: Welcome series
│   ├── reengagement.py     # Sequence2: Reactivar leads fríos
│   ├── behavioral.py       # Sequence3: Triggers de contenido
│   └── abandoned.py        # Sequence4: Recuperar abandonados
├── templates/
│   ├── welcome/            # Plantillas HTML welcome
│   ├── reengagement/       # Plantillas HTML reengagement
│   ├── behavioral/          # Plantillas HTML behavioral
│   └── abandoned/           # Plantillas HTML abandoned
└── main.py                 # Orquestador principal

requirements.txt

text
pandas==2.1.0
requests==2.31.0
python-dotenv==1.0.0
jinja2==3.1.2
sendgrid==6.10.0  # O mailchimp==3.0.0
sqlalchemy==2.0.0  # Para database opcional

models/lead.py - Modelo de datos

python
from datetime import datetime
from enum import Enum

class LeadStage(Enum):
    NEW = "new"
    ENGAGED = "engaged"
    QUALIFIED = "qualified"
    CUSTOMER = "customer"
    CHURNED = "churned"

class LeadIntent(Enum):
    AWARENESS = "awareness"    # Búsqueda informativa
    CONSIDERATION = "consideration"  # Comparativa
    DECISION = "decision"      # Lista para comprar
    UNKNOWN = "unknown"

class Lead:
    def __init__(self, email, first_name=None, source="seo", utm_params=None):
        self.email = email
        self.first_name = first_name or email.split('@')[0]
        self.source = source
        self.utm_params = utm_params or {}
        
        # Estados
        self.stage = LeadStage.NEW
        self.intent = self._detect_intent_from_utm()
        
        # Metadata
        self.created_at = datetime.now()
        self.last_activity = self.created_at
        self.emails_sent = 0
        self.emails_opened = 0
        self.pages_visited = []
        self.score = 0
    
    def _detect_intent_from_utm(self):
        """Detecta intención basada en parámetros UTM"""
        utm_content = self.utm_params.get('utm_content', '').lower()
        
        if any(kw in utm_content for kw in ['cómo', 'qué es', 'guía', 'tutorial']):
            return LeadIntent.AWARENESS
        
        if any(kw in utm_content for kw in ['mejor', 'precio', 'vs', 'comparar', 'alternativa']):
            return LeadIntent.CONSIDERATION
        
        if any(kw in utm_content for kw in ['contratar', 'comprar', 'agencia', 'servicio']):
            return LeadIntent.DECISION
        
        return LeadIntent.UNKNOWN
    
    def to_dict(self):
        return {
            'email': self.email,
            'first_name': self.first_name,
            'source': self.source,
            'stage': self.stage.value,
            'intent': self.intent.value,
            'score': self.score,
            'created_at': self.created_at.isoformat(),
            'last_activity': self.last_activity.isoformat()
        }

Integración SEO → Email: Captura y Segmentación

El primer paso es capturar leads que vienen de SEO con suficiente contexto para personalizar.

services/capture.py - Capturar leads con contexto SEO

python
from urllib.parse import parse_qs
from models.lead import Lead, LeadIntent

def capture_lead_from_utm(utm_params, email, first_name=None):
    """
    Captura lead con parámetros UTM de Google Analytics/Ads
    
    Args:
        utm_params: Dict con parámetros UTM
        email: Email del lead
        first_name: Nombre opcional
    
    Returns:
        Objeto Lead con contexto SEO detectado
    """
    lead = Lead(
        email=email,
        first_name=first_name,
        source="seo",
        utm_params=utm_params
    )
    
    # Detectar intención basada en UTM content
    content = utm_params.get('utm_content', '').lower()
    
    if 'blog' in content or 'artículo' in content:
        lead.intent = LeadIntent.AWARENESS
        lead.stage = LeadStage.NEW
    elif 'servicio' in content or 'producto' in content:
        lead.intent = LeadIntent.CONSIDERATION
    elif 'contacto' in content or 'presupuesto' in content:
        lead.intent = LeadIntent.DECISION
        lead.stage = LeadStage.QUALIFIED
    
    # Extraer metadata adicional
    lead.utm_params['source'] = utm_params.get('utm_source', 'organic')
    lead.utm_params['medium'] = utm_params.get('utm_medium', 'none')
    lead.utm_params['campaign'] = utm_params.get('utm_campaign', 'none')
    
    return lead

def capture_lead_from_gsc(query, email, first_name=None):
    """
    Captura lead basado en query de búsqueda GSC
    
    Args:
        query: Query de búsqueda que trajo el tráfico
        email: Email del lead
        first_name: Nombre opcional
    
    Returns:
        Objeto Lead con intención detectada
    """
    lead = Lead(
        email=email,
        first_name=first_name,
        source="gsc_organic",
        utm_params={'query': query}
    )
    
    # Detectar intención desde query
    query_lower = query.lower()
    
    # Patrones de intención
    awareness_patterns = ['cómo', 'qué es', 'guía', 'tutorial', 'mejor forma de']
    consideration_patterns = ['mejor', 'precio', 'vs', 'comparar', 'alternativa a']
    decision_patterns = ['contratar', 'comprar', 'agencia', 'servicio de', 'proveedor']
    
    if any(pattern in query_lower for pattern in awareness_patterns):
        lead.intent = LeadIntent.AWARENESS
    elif any(pattern in query_lower for pattern in consideration_patterns):
        lead.intent = LeadIntent.CONSIDERATION
    elif any(pattern in query_lower for pattern in decision_patterns):
        lead.intent = LeadIntent.DECISION
        lead.stage = LeadStage.QUALIFIED
    else:
        lead.intent = LeadIntent.UNKNOWN
    
    return lead

services/score.py - Lead scoring basado en comportamiento

python
from models.lead import LeadStage

def calculate_lead_score(lead):
    """
    Calcula score de lead (0-100) basado en interacción
    
    Factores:
    - Emails abiertos: +30 puntos máximo
    - Páginas visitadas: +20 puntos máximo
    - Tiempo desde última actividad: -10 puntos por semana de inactividad
    - Intención de búsqueda: +20 puntos (DECISION > CONSIDERATION > AWARENESS)
    - Etapa del funnel: +15 puntos (CUSTOMER > QUALIFIED > ENGAGED > NEW)
    """
    score = 0
    
    # 1. Engagement (emails abiertos)
    if lead.emails_sent > 0:
        open_rate = lead.emails_opened / lead.emails_sent
        score += open_rate * 30  # Máximo 30 puntos
    
    # 2. Profundidad (páginas visitadas)
    unique_pages = len(set(lead.pages_visited))
    score += min(unique_pages * 5, 20)  # Máximo 20 puntos
    
    # 3. Recencia (actividad reciente)
    days_inactive = (datetime.now() - lead.last_activity).days
    recency_score = max(0, 100 - days_inactive * 10)
    score += recency_score * 0.15  # Máximo 15 puntos
    
    # 4. Intención de búsqueda
    intent_scores = {
        'awareness': 5,
        'consideration': 15,
        'decision': 20,
        'unknown': 0
    }
    score += intent_scores.get(lead.intent.value, 0)
    
    # 5. Etapa del funnel
    stage_scores = {
        'new': 0,
        'engaged': 5,
        'qualified': 15,
        'customer': 20
    }
    score += stage_scores.get(lead.stage.value, 0)
    
    lead.score = min(int(score), 100)  # Máximo 100
    return lead.score

def segment_leads(leads):
    """
    Segmenta leads en categorías para nurturing
    
    Returns:
        Dict con listas de leads por segmento
    """
    segments = {
        'hot_leads': [],      # Score > 70, etapa DECISION
        'warm_leads': [],    # Score 40-70, etapa CONSIDERATION
        'cold_leads': [],     # Score < 40, etapa AWARENESS
        'dormant': [],        # Sin actividad > 30 días
        'ready_to_buy': []    # Score > 80, etapa DECISION, activo recientemente
    }
    
    for lead in leads:
        score = calculate_lead_score(lead)
        
        # Clasificación principal
        if score > 70 and lead.stage.value == 'qualified':
            if (datetime.now() - lead.last_activity).days <= 7:
                segments['ready_to_buy'].append(lead)
            segments['hot_leads'].append(lead)
        elif score > 40:
            segments['warm_leads'].append(lead)
        else:
            segments['cold_leads'].append(lead)
        
        # Detectar inactivos
        if (datetime.now() - lead.last_activity).days > 30:
            segments['dormant'].append(lead)
    
    return segments

Sequence 1: Welcome Series

Propósito: 5-email sequence para nuevos leads que acaban de llegar. Educa, construye confianza, prepara para venta.

Estructura:

  • Email 1 (Día 0): Welcome + contenido valor inmediato según búsqueda
  • Email 2 (Día 2): Introducción a tu empresa + social proof
  • Email 3 (Día 4): Contenido educativo (según intención de búsqueda)
  • Email 4 (Día 7): Case study relevante + testimonio
  • Email 5 (Día 10): Oferta suave + CTA

sequences/welcome.py - Script completo

python
from jinja2 import Template
from datetime import datetime, timedelta
from services.esp import send_email

def render_welcome_email(lead, email_number, content_type):
    """
    Renderiza email de welcome series
    
    Args:
        lead: Objeto Lead
        email_number: 1-5
        content_type: 'awareness' | 'social_proof' | 'education' | 'case_study' | 'offer'
    
    Returns:
        HTML del email personalizado
    """
    templates = {
        1: Template('''
            <h1>Bienvenido a {{ company_name }}</h1>
            <p>Gracias por suscribirte, {{ first_name }}.</p>
            
            {% if search_query %}
            <p>Veo que estás interesado en <strong>{{ search_query }}</strong>. He preparado recursos específicos sobre este tema.</p>
            {% endif %}
            
            <p>En los próximos días, te compartiré:</p>
            <ul>
                <li>Guías exclusivas sobre {{ search_query }}</li>
                <li>Case studies de clientes como tú</li>
                <li>Ofertas especiales para nuevos suscriptores</li>
            </ul>
            
            <p>Si no puedes esperar, aquí tienes el recurso principal:</p>
            <p><a href="{{ main_resource_url }}">{{ main_resource_title }}</a></p>
        '''),
        
        2: Template('''
            <h2>Un poco sobre nosotros</h2>
            <p>Hola {{ first_name }},</p>
            
            <p>Quiero presentarte {{ company_name }}. Somos especialistas en {{ main_service }} con {{ years_experience }} años de experiencia.</p>
            
            <h3>Lo que nos diferencia:</h3>
            <ul>
                {% for differentiator in unique_value_props %}
                <li>{{ differentiator }}</li>
                {% endfor %}
            </ul>
            
            <p><strong>Nuestro compromiso:</strong> {{ guarantee_statement }}</p>
            
            <p>Conoce nuestros resultados:</p>
            <p><a href="{{ case_study_url }}">Ver case study →</a></p>
        '''),
        
        3: Template('''
            <h2>{{ resource_title }}: Guía Completa</h2>
            <p>Hola {{ first_name }},</p>
            
            <p>En {{ company_name }} creemos que la mejor forma de aprender es haciendo. Por eso, he preparado esta guía paso a paso sobre {{ search_query }}.</p>
            
            <h2>Lo que aprenderás:</h2>
            <ol>
                {% for learning_point in learning_points %}
                <li>{{ learning_point }}</li>
                {% endfor %}
            </ol>
            
            <h2>Paso 1: Fundamentos</h2>
            <p>{{ step1_content }}</p>
            
            <h2>Paso 2: Estrategia</h2>
            <p>{{ step2_content }}</p>
            
            <h2>Paso 3: Implementación</h2>
            <p>{{ step3_content }}</p>
            
            <p>Si necesitas ayuda con la implementación, responde a este email. Estoy aquí para ayudar.</p>
        '''),
        
        4: Template('''
            <h2>Cómo {{ client_name }} logró {{ result }}</h2>
            <p>Hola {{ first_name }},</p>
            
            <p>Quería compartirte una historia real. {{ client_name }} tenía el mismo desafío que tú: {{ client_challenge }}.</p>
            
            <h3>Antes:</h3>
            <p>{{ before Situation }}</p>
            
            <h3>Qué hicimos:</h3>
            <p>{{ solution_implemented }}</p>
            
            <h3>Resultados:</h3>
            <ul>
                {% for result in results_achieved %}
                <li>{{ result }}</li>
                {% endfor %}
            </ul>
            
            <p><em>Testimonio verificado.</em></p>
            
            <p>¿Quieres resultados similares? Respondamos y hablemos.</p>
        '''),
        
        5: Template('''
            <h2>Oferta especial para ti, {{ first_name }}</h2>
            <p>Gracias por seguir nuestra serie, {{ first_name }}.</p>
            
            <p>Como valoro tu interés, quiero hacerte una oferta exclusiva:</p>
            
            <div style="background: #f0f8ff; padding: 20px; border-left: 4px solid #4285f4;">
                <h3>{{ offer_title }}</h3>
                <p>{{ offer_description }}</p>
                <p><strong>Valor habitual:</strong> <del>{{ regular_price }}</del></p>
                <p><strong>Precio especial:</strong> <strong>{{ special_price }}</strong></p>
                <p><strong>Ahorro:</strong> {{ savings }}</p>
                <p><strong>Válido por:</strong> {{ validity_days }} días</p>
            </div>
            
            <p><strong>Código:</strong> {{ promo_code }}</p>
            
            <p><a href="{{ cta_url }}">{{ cta_button_text }} →</a></p>
            
            <p><em>Oferta válida hasta {{ expiry_date }}.</em></p>
        ''')
    }
    
    template = templates.get(email_number)
    
    # Renderizar con datos del lead
    html = template.render(
        company_name="4SEO.es",
        first_name=lead.first_name,
        search_query=lead.utm_params.get('query', 'SEO'),
        main_resource_url="https://4seo.es/blog",
        years_experience=12,
        main_service="automatización SEO",
        unique_value_props=[
            "Enfoque data-driven, no intuiciones",
            "Clientes que duplican tráfico en 6 meses",
            "Especialistas en sitios de 100K+ páginas"
        ],
        guarantee_statement="Mejoramos tus rankings o te devolvemos tu dinero.",
        case_study_url="https://4seo.es/casos-exito",
        client_name="Empresa X",
        client_challenge="bajo tráfico y conversiones",
        before Situation="10 visitas/día, 0.5% conversión",
        solution_implemented="Auditoría técnica + estrategia de contenidos",
        results_achieved=[
            "+300% tráfico en 6 meses",
            "CTR de 0.5% a 4.5%",
            "15 leads cualificados/mes"
        ],
        offer_title="Auditoría SEO Gratuita",
        offer_description="Análisis completo de tu sitio + estrategia personalizada",
        regular_price="497€",
        special_price="0€",
        savings="497€",
        validity_days=7,
        promo_code="BIENVENIDO4SEO",
        cta_url="https://4seo.es/contacto",
        cta_button_text="Solicitar Auditoría Gratuita",
        expiry_date=(datetime.now() + timedelta(days=14)).strftime('%d/%m/%Y')
    )
    
    return html

def schedule_welcome_sequence(lead):
    """Programa welcome series para un lead
    
    Args:
        lead: Objeto Lead
    
    Returns:
        Dict con scheduled emails
    """
    schedule = []
    
    # Email 1: Inmediato
    schedule.append({
        'lead_id': lead.email,
        'email_number': 1,
        'send_at': datetime.now(),
        'template_type': 'welcome'
    })
    
    # Email 2: +2 días
    schedule.append({
        'lead_id': lead.email,
        'email_number': 2,
        'send_at': datetime.now() + timedelta(days=2),
        'template_type': 'social_proof'
    })
    
    # Email 3: +4 días
    schedule.append({
        'lead_id': lead.email,
        'email_number': 3,
        'send_at': datetime.now() + timedelta(days=4),
        'template_type': 'education'
    })
    
    # Email 4: +7 días
    schedule.append({
        'lead_id': lead.email,
        'email_number': 4,
        'send_at': datetime.now() + timedelta(days=7),
        'template_type': 'case_study'
    })
    
    # Email 5: +10 días
    schedule.append({
        'lead_id': lead.email,
        'email_number': 5,
        'send_at': datetime.now() + timedelta(days=10),
        'template_type': 'offer'
    })
    
    return schedule

Sequence 2: Re-engagement

Propósito: Reactivar leads que han estado inactivos por más de 30 días. Estrategia suave, no agresiva.

sequences/reengagement.py - Script

python
from datetime import datetime, timedelta
from jinja2 import Template

def render_reengagement_email(lead, context_type="check_in"):
    """
    Renderiza email de re-engagement
    
    Args:
        lead: Objeto Lead
        context_type: 'check_in' | 'value' | 'we_miss_you'
    
    Returns:
        HTML del email
    """
    templates = {
        'check_in': Template('''
            <h2>¿Cómo va todo, {{ first_name }}?</h2>
            <p>Hace {{ days_inactive }} días que no vemos por aquí.</p>
            
            <p>Estuve pensando en ti. Cuando te suscribiste, estabas interesado en {{ search_query }}.</p>
            
            <p>¿Sigues trabajando en eso? ¿Encontraste lo que buscabas?</p>
            
            <p>Si necesitas ayuda, estoy aquí. Responde a este email y te ayudaré.</p>
        '''),
        
        'value': Template('''
            <h2>Algo nuevo desde la última vez</h2>
            <p>Hola {{ first_name }},</p>
            
            <p>Ha pasado un tiempo, así que quería compartir actualizaciones sobre {{ search_query }}:</p>
            
            <h3>Novedades:</h3>
            <ul>
                {% for update in updates %}
                <li>{{ update }}</li>
                {% endfor %}
            </ul>
            
            <p>Quizás estas actualizaciones te interesan:</p>
            <ul>
                {% for resource in new_resources %}
                <li><a href="{{ resource.url }}">{{ resource.title }}</a></li>
                {% endfor %}
            </ul>
        '''),
        
        'we_miss_you': Template('''
            <h2>Te echamos de menos, {{ first_name }}</h2>
            <p>Es verdad. Hace {{ days_inactive }} días que no interactuamos.</p>
            
            <p>No quiero ser molesto. Solo quería decirte que si vuelves, tengo nuevos recursos para ti:</p>
            
            <ul>
                {% for resource in returning_resources %}
                <li><strong>{{ resource.title }}:</strong> <a href="{{ resource.url }}">Ver ahora</a></li>
                {% endfor %}
            </ul>
            
            <p>Espero verte pronto.</p>
            
            <p>Si no, te entiendo. Un saludo.</p>
        ''')
    }
    
    template = templates.get(context_type)
    
    days_inactive = (datetime.now() - lead.last_activity).days
    
    html = template.render(
        first_name=lead.first_name,
        days_inactive=days_inactive,
        search_query=lead.utm_params.get('query', 'SEO'),
        updates=[
            "Nuevo artículo sobre técnicas avanzadas",
            "Cambio en algoritmo de Google",
            "Nueva herramienta de análisis"
        ],
        new_resources=[
            {
                "title": "Guía actualizada 2026",
                "url": "https://4seo.es/blog"
            }
        ],
        returning_resources=[
            {
                "title": "Webinar exclusivo: Tendencias SEO 2026",
                "url": "https://4seo.es/webinar"
            }
        ]
    )
    
    return html

def identify_dormant_leads(leads, inactive_days=30):
    """Identifica leads inactivos para re-engagement
    
    Args:
        leads: Lista de objetos Lead
        inactive_days: Días sin actividad para considerar inactivo
    
    Returns:
        Lista de leads inactivos
    """
    dormant = []
    cutoff_date = datetime.now() - timedelta(days=inactive_days)
    
    for lead in leads:
        if lead.last_activity < cutoff_date:
            dormant.append(lead)
    
    return dormant

# Script de re-engagement
if __name__ == '__main__':
    from services.capture import capture_lead_from_gsc
    from services.score import calculate_lead_score
    
    # Simular carga de leads
    leads = [
        capture_lead_from_gsc("auditoría seo barcelona", "[email protected]", "María"),
        capture_lead_from_gsc("servicios seo", "[email protected]", "Juan")
    ]
    
    # Marcar como inactivos (para demo)
    from datetime import timedelta
    leads[0].last_activity = datetime.now() - timedelta(days=45)
    
    # Identificar inactivos
    dormant = identify_dormant_leads(leads)
    
    print(f"{len(dormant)} leads inactivos encontrados")
    
    for lead in dormant:
        print(f"\n📧 Enviando re-engagement a {lead.email}...")
        
        # Enviar email de check-in
        html = render_reengagement_email(lead, 'check_in')
        # send_email(lead.email, "¿Cómo va?", html)
        
        print(f"Email enviado a {lead.email}")

Sequence 3: Behavioral Triggers

Propósito: Emails automáticos activados por comportamiento específico del usuario (página visitada, descarga, click).

sequences/behavioral.py - Script

python
from jinja2 import Template

def trigger_behavioral_email(lead, trigger_type, context_data):
    """
    Dispara y renderiza email basado en trigger conductual
    
    Args:
        lead: Objeto Lead
        trigger_type: 'page_visit' | 'download' | 'cart_abandon' | 'quote_request'
        context_data: Datos adicionales del trigger
    
    Returns:
        HTML del email personalizado
    """
    if trigger_type == 'page_visit':
        template = Template('''
            <h2>Vi que te interesa {{ page_title }}</h2>
            <p>Hola {{ first_name }},</p>
            
            <p>Noté que visitaste <strong>{{ page_title }}</strong>. Contiene información valiosa sobre {{ page_topic }}.</p>
            
            <p>Basado en tu interés, creo que te gustaría:</p>
            <ul>
                {% for next_step in suggested_next_steps %}
                <li>{{ next_step }}</li>
                {% endfor %}
            </ul>
            
            <p>¿Quieres que te lo envíe?</p>
            <p><a href="{{ download_link }}">Descargar recurso →</a></p>
        ''')
        
        html = template.render(
            first_name=lead.first_name,
            page_title=context_data.get('page_title', 'esta página'),
            page_topic="optimización de conversión",
            suggested_next_steps=[
                "Guía paso a paso",
                "Checklist de implementación",
                "Herramientas recomendadas"
            ],
            download_link="https://4seo.es/resources/download-guide"
        )
    
    elif trigger_type == 'download':
        template = Template('''
            <h2>¡Gracias por descargar {{ resource_title }}!</h2>
            <p>Hola {{ first_name }},</p>
            
            <p>Espero que {{ resource_title }} te sea útil.</p>
            
            <p>Muchos clientes tienen preguntas después de usar esta guía:</p>
            <ul>
                {% for faq in common_questions %}
                <li><strong>{{ faq.question }}</strong></li>
                <p>{{ faq.answer }}</p></li>
                {% endfor %}
            </ul>
            
            <p>Si tienes alguna duda, responde a este email.</p>
            
            <p><strong>Siguiente paso recomendado:</strong></p>
            <p>{{ next_step }}</p>
            <p><a href="{{ next_step_url }}">Ver guía →</a></p>
        ''')
        
        html = template.render(
            first_name=lead.first_name,
            resource_title=context_data.get('resource_title', 'Guía SEO'),
            common_questions=[
                {'question': '¿Cuánto tiempo tarda en ver resultados?', 'answer': 'Normalmente 4-12 semanas.'},
                {'question': '¿Necesito conocimientos técnicos?', 'answer': 'No, las guías son paso a paso.'}
            ],
            next_step="Implementar la primera optimización del guide",
            next_step_url="https://4seo.es/blog/como-implementar"
        )
    
    elif trigger_type == 'cart_abandon':
        template = Template('''
            <h2>¿Todavía interesado en {{ product_or_service }}?</h2>
            <p>Hola {{ first_name }},</p>
            
            <p>Vi que iniciaste el proceso de {{ product_or_service }} pero no completaste.</p>
            
            <p>Entiendo. Es una decisión importante. Tómate tu tiempo.</p>
            
            <p>Si aún estás interesado, puedo ofrecerte:</p>
            <ul>
                {% for incentive => incentives %}
                <li>{{ incentive }}</li>
                {% endfor %}
            </ul>
            
            <p>La oferta expira en {{ hours_to_expire }} horas.</p>
            
            <p><a href="{{ cart_url }}">Completar proceso →</a></p>
        ''')
        
        html = template.render(
            first_name=lead.first_name,
            product_or_service="auditoría SEO",
            incentives=[
                "10% de descuento adicional",
                "LLamada de estrategia gratuita incluida",
                "Prioridad en implementación"
            ],
            hours_to_expire=48,
            cart_url="https://4seo.es/contacto?utm_source=re-engagement"
        )
    
    elif trigger_type == 'quote_request':
        template = Template('''
            <h2>Tu solicitud de presupuesto</h2>
            <p>Hola {{ first_name }},</p>
            
            <p>Gracias por solicitar presupuesto para {{ service_type }}.</p>
            
            <p>Estoy revisando tu solicitud. Te responderé en {{ response_time }>.</p>
            
            <p>Mientras tanto, quizás te interesa:</p>
            <ul>
                {% for resource in helpful_resources %}
                <li><a href="{{ resource.url }}">{{ resource.title }}</a></li>
                {% endfor %}
            </ul>
        ''')
        
        html = template.render(
            first_name=lead.first_name,
            service_type="auditoría técnica",
            response_time="24-48 horas",
            helpful_resources=[
                {'title': 'Portfolio de casos de éxito', 'url': 'https://4seo.es/casos-exito'},
                {'title': 'Proceso de trabajo', 'url': 'https://4seo.es/metodologia'}
            ]
        )
    
    return html

Detectar triggers automáticamente

python
# services/trigger.py
from datetime import datetime, timedelta

def detect_abandoned_cart(leads, cart_expiry_hours=24):
    """
    Detecta leads con carritos abandonados
    
    Args:
        leads: Lista de objetos Lead
        cart_expiry_hours: Horas después de las cuales se considera abandonado
    
    Returns:
        Lista de leads con carrito abandonado
    """
    abandoned = []
    
    for lead in leads:
        # Si añadió productos al carrito pero no compró
        if hasattr(lead, 'cart_items') and lead.cart_items:
            cart_time = lead.cart_added_at
            expiry_time = cart_time + timedelta(hours=cart_expiry_hours)
            
            if datetime.now() > expiry_time and not lead.purchased:
                abandoned.append(lead)
    
    return abandoned

def detect_page_visit_triggers(leads, page_url):
    """
    Detecta leads que visitaron una página específica
    
    Args:
        leads: Lista de objetos Lead
        page_url: Página a monitorear
    
    Returns:
    Lista de leads que visitaron la página
    """
    triggers = []
    
    for lead in leads:
        if page_url in lead.pages_visited:
            # Verificar si no es spam (visitas recientes múltiples)
            recent_visits = [p for p in lead.pages_visited if p == page_url]
            
            if len(recent_visits) == 1:  # Visitó una vez, parece genuino
                triggers.append({
                    'lead': lead,
                    'trigger_type': 'page_visit',
                    'triggered_at': datetime.now(),
                    'context': {'page_title': 'Página de servicios'}
                })
    
    return triggers

Sequence 4: Abandoned Content Recovery

Propósito: Recuperar leads que empezaron a consumir contenido pero no terminaron.

sequences/abandoned.py - Script

python
from jinja2 import Template

def render_abandoned_content_email(lead, content_type, content_data):
    """
    Renderiza email de recuperación de contenido abandonado
    
    Args:
        lead: Objeto Lead
        content_type: 'guide' | 'course' | 'webinar'
        content_data: Dict con datos del contenido
    
    Returns:
        HTML del email
    """
    templates = {
        'guide': Template('''
            <h2>¿Te perdiste en esta guía?</h2>
            <p>Hola {{ first_name }},</p>
            
            <p>Vi que empezaste a leer <strong>{{ guide_title }}</strong> pero no lo terminaste.</p>
            
            <p>Es normal. A veces la vida nos interrumpe. Pero quería recordarte que esta guía sigue aquí cuando estés listo.</p>
            
            <p><strong>Dónde te quedaste:</strong></p>
            <p>{{ last_section_title }}</p>
            <p>Avance: {{ progress_percentage }}% completado</p>
            
            <p><strong>Por qué debería terminarla:</strong></p>
            <ul>
                {% for benefit in key_benefits %}
                <li>{{ benefit }}</li>
                {% endfor %}
            </ul>
            
            <p><a href="{{ resume_url }}">Continuar desde donde lo dejaste →</a></p>
        '''),
        
        'course': Template('''
            <h2>Tu curso te está esperando, {{ first_name }}</h2>
            <p>Te inscribiste en <strong>{{ course_name }}</strong> pero no has comenzado.</p>
            
            <p>Entiendo. El momento perfecto nunca llega. Mejor empezar tarde que nunca.</p>
            
            <p><strong>Lo que aprenderás:</strong></p>
            <ul>
                {% for module in course_modules %}
                <li>{{ module }}</li>
                {% endfor %}
            </ul>
            
            <p><strong>Incluye:</strong></p>
            <ul>
                {% for bonus in bonuses %}
                <li>{{ bonus }}</li>
                {% endfor %}
            </ul>
            
            <p><a href="{{ start_url }}">Comenzar ahora →</a></p>
        ''')
    }
    
    template = templates.get(content_type)
    html = template.render(
        first_name=lead.first_name,
        guide_title=content_data.get('title', 'Guía SEO'),
        last_section_title=content_data.get('last_section', 'Introducción'),
        progress_percentage=content_data.get('progress', 0),
        key_benefits=[
            "Estrategias probadas que funcionan",
            "Casos de éxito reales",
            "Checklists descargables"
        ],
        resume_url=content_data.get('url', ''),
        course_name=content_data.get('course', 'Curso SEO Avanzado'),
        course_modules=[
            "Módulo 1: Fundamentos",
            "Módulo 2: Estrategia",
            "Módulo 3: Tácticas avanzadas"
        ],
        bonuses=[
            "Plantillas descargables",
            "Sesión de Q&A grupal",
            "Certificado de finalización"
        ],
        start_url=content_data.get('start_url', '')
    )
    
    return html

Integraciones ESP: Mailchimp, SendGrid, Self-Hosted

services/esp.py - Conexión con Email APIs

python
# services/esp.py - SendGrid integration
import sendgrid
from sendgrid.helpers import Mail
from config import config

def send_email_sendgrid(to_email, subject, html_content):
    """Envía email usando SendGrid API
    
    Args:
        to_email: Email del destinatario
        subject: Asunto del email
        html_content: Contenido HTML
    
    Returns:
        Response de SendGrid
    """
    message = Mail(
        from_email=config.EMAIL_FROM,
        to_emails=to_email,
        subject=subject,
        html_content=html_content
    )
    
    sg = sendgrid.SendGridAPIClient(config.SENDGRID_API_KEY)
    response = sg.send(message)
    
    return response

# services/esp.py - Mailchimp integration (opcional)
def add_lead_to_mailchimp(email, first_name, list_id):
    """Añade lead a lista en Mailchimp
    
    Args:
        email: Email del lead
        first_name: Nombre del lead
        list_id: ID de lista de Mailchimp
    
    Returns:
        Response de Mailchimp
    """
    import mailchimp3
    
    client = mailchimp.Mailchimp(config.MAILCHIMP_API_KEY)
    
    # Añadir a lista
    response = client.lists.members.create(
        list_id=list_id,
        data={            'email_address': email,
            'status': 'subscribed',
            'merge_fields': {
                'FNAME': first_name
            }
        }
    )
    
    return response

Opción Self-Hosted: SMTP propio

python
<n># services/esp.py - SMTP self-hosted
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_email_smtp(to_email, subject, html_content):
    """Envía email usando SMTP propio
    
    Args:
        to_email: Email del destinatario
        subject: Asunto del email
        html_content: Contenido HTML
    
    Returns:
        Response (sent_ok, message)
    """
    # Configuración SMTP (en .env)
    smtp_server = config.SMTP_SERVER  # ej: smtp.gmail.com
    smtp_port = config.SMTP_PORT        # ej: 587
    smtp_user = config.SMTP_USER      # tu email
    smtp_password = config.SMTP_PASSWORD  # tu app password
    
    # Crear mensaje
    msg = MIMEMultipart('alternative')
    msg['Subject'] = subject
    msg['From'] = config.EMAIL_FROM
    msg['To'] = to_email
    
    msg.attach(MIMEText('text', 'plain'))
    text_part = f"{subject}\n\nVer este email en un cliente de email que soporte HTML."
    msg.attach(MIMEText('html', 'html'))
    html_part = MIMEText(html_content, 'html')
    msg.attach(html_part)
    
    # Enviar
    with smtplib.SMTP(smtp_server, smtp_port) as server:
        server.starttls()
        server.login(smtp_user, smtp_password)
        server.send_message(msg)
        server.quit()
    
    return {'sent_ok': True, 'message': 'Email enviado'}

Métricas y ROI

Dashboard de métricas

python
# services/analytics.py
import pandas as pd
from datetime import datetime, timedelta

def calculate_nurturing_metrics(leads, period_days=30):
    """Calcula métricas de nurturing para un período
    
    Args:
        leads: Lista de objetos Lead
        period_days: Días del período
    
    Returns:
        Dict con métricas agregadas
    """
    start_date = datetime.now() - timedelta(days=period_days)
    
    # Filtrar leads activos en el período
    active_leads = [l for l in leads if l.created_at >= start_date]
    
    if not active_leads:
        return None
    
    # Métricas de email
    total_emails = sum(l.emails_sent for l in active_leads)
    total_opens = sum(l.emails_opened for l in active_leads)
    total_clicks = sum(l.emails_clicked for l in active_leads)
    open_rate = (total_opens / total_emails * 100) if total_emails > 0 else 0
    click_rate = (total_clicks / total_opens * 100) if total_opens > 0 else 0
    
    # Métricas de conversión
    total_conversions = sum(1 for l in active_leads if l.stage.value == 'customer')
    conversion_rate = (total_conversions / len(active_leads) * 100) if active_leads else 0
    
    # ROI estimado (si conocemos costos)
    # ROI = (Revenue - Cost) / Cost * 100
    # Simplificado aquí
    avg_revenue_per_conversion = 497  # Precio medio de servicio
    total_revenue = total_conversions * avg_revenue_per_conversion
    
    metrics = {
        'period': f"Últimos {period_days} días",
        'total_leads': len(active_leads),
        'emails_sent': total_emails,
        'emails_opened': total_opens,
        'emails_clicked': total_clicks,
        'open_rate': round(open_rate, 2),
        'click_rate': round(click_rate, 2),
        'conversions': total_conversions,
        'conversion_rate': round(conversion_rate, 2),
        'estimated_revenue': total_revenue
    }
    
    return metrics

def generate_html_report(metrics):
    """Genera reporte HTML de métricas"""
    template = Template('''
        <!DOCTYPE html>
        <html>
        
        <body>
        <h1>📊 Lead Nurturing Report - {{ period }}</h1>
        
        <div class="metric">
            <h3>Resumen</h3>
            <p><strong>Leads activos:</strong> {{ total_leads }}</p>
            <p><strong>Emails enviados:</strong> {{ emails_sent }}</p>
            <p><strong>Tasa apertura:</strong> {{ open_rate }}%</p>
            <p><strong>Conversiones:</strong> {{ conversions }} ({{ conversion_rate }}%)</p>
        </div>
        
        <h2>Métricas por Segmento</h2>
        <table>
            <tr>
                <th>Segmento</th>
                <th>Leads</th>
                <th>Conversión</th>
            </tr>
            {% for segment in segments %}
            <tr>
                <td>{{ segment.name }}</td>
                <td>{{ segment.count }}</td>
                <td>{{ segment.conversion_rate }}%</td>
            </tr>
            {% endfor %}
        </table>
        
        <p><em>Generado: {{ generated_at }}</em></p>
        </body>
        </html>
    ''')
    
    html = template.render(
        period=metrics['period'],
        total_leads=metrics['total_leads'],
        emails_sent=metrics['emails_sent'],
        open_rate=metrics['open_rate'],
        conversions=metrics['conversions'],
        conversion_rate=metrics['conversion_rate'],
        generated_at=datetime.now().strftime('%Y-%m-%d %H:%M'),
        segments=[
            {'name': 'Hot Leads', 'count': 5, 'conversion_rate': 12.5},
            {'name': 'Warm Leads', 'count': 15, 'conversion_rate': 3.2},
            {'name': 'Cold Leads', 'count': 30, 'conversion_rate': 0.5}
        ]
    )
    
    return html

Conclusión

Con este framework de lead nurturing automatizado, tienes un sistema completo que:

  • Conecta SEO con Email: Captura intención de búsqueda automáticamente
  • Personaliza contenido: Cada lead recibe según su búsqueda
  • Sequences automatizadas: Welcome, Re-engagement, Behavioral, Abandoned Content
  • Segmentación dinámica: Clasifica leads según comportamiento
  • Medición completa: Métricas de email, conversión, ROI

Casos de uso del framework

Próximos pasos

  • Añade más triggers: Descargas de recursos específicos, tiempo en página, scroll depth
  • Integra con CRM: HubSpot, Salesforce, Pipedrive para sincronizar estados
  • Personalización avanzada: Contenido dinámico según lead scoring + stage
  • Multicanal: Añade soporte para emails en múltiples idiomas

Recursos recomendados

  • Mailchimp Automation
  • SendGrid Marketing
  • HubSpot Workflows

Sobre el Autor

Alfonso Calero Gijón es Editor de 4SEO.es, portal de noticias y comunidad SEO. Especialista en automatización de procesos SEO y arquitectura de información. Escribe análisis y guías sobre automatización SEO para la comunidad.

Conectar en LinkedIn →

¿Necesitas ayuda con automatización SEO?

Sigue en 4SEO → Más guías en el blog

Blog 4SEO → Más guías en el blog

Blog 4SEO → Más guías de SEO técnico

Artículos Relacionados

  • CRO para Páginas de Servicios
  • SaaS B2B Lead Generation
  • Python para SEO: Automatiza tu Trabajo Diario

Ver como Markdown