Cómo construí el sitio de un despacho de abogados con 37 landings (y tres intentos hasta acertar con el email)
¿Cómo se construye un sitio de alta conversión para un despacho de abogados cuando el primer, segundo y hasta tercer intento de resolver algo tan simple como un formulario de contacto no funcionan como se planearon?
Por Pietro Fiorillo ·
Índice del artículo
El problema: generar consultas para un despacho que dependía solo de referencias
El despacho tenía experiencia real gestionando reclamaciones por accidentes de tráfico y siniestros de seguros, pero dependía por completo de recomendaciones boca a boca. Sin presencia digital ni captación activa, el crecimiento tenía un techo bajo.
Por qué 37 landings y no una sola página de servicios
El sitio tiene 37 carpetas de ruta, la mayoría landings por tipo de accidente: choques, atropellos, siniestros con fuga, accidentes laborales in itinere y 19 tipos distintos de indemnización (amputación, paraplejia, tetraplejia, látigo cervical, entre otros). Cada una compite por una búsqueda específica en vez de todas competir por los mismos términos genéricos.
A eso se suma un blog de 6 artículos en 4 categorías (Guías, Legislación, Lesiones, Tipos de Accidentes) para captar tráfico de gente que todavía no sabe que tiene un caso.
Los tres intentos de resolver algo tan simple como un formulario de contacto
Esta es la parte que normalmente no se cuenta en un caso de éxito, y es la que mejor explica cómo se construye software de verdad. El formulario de contacto pasó por tres implementaciones distintas antes de llegar a la que funciona hoy.
El primer intento fue una Cloud Function que recibía el formulario y mandaba el email con Nodemailer, guardando además el lead en una colección de Firestore. Funciona como función aislada, pero nunca se llama desde el frontend en producción: quedó construida y sin conectar.
const db = admin.firestore();
const docRef = await db.collection("leads").add({
nombre, email,
telefono: telefono || "",
asunto: asunto || "",
mensaje,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
ip: req.ip || "",
userAgent: req.get("user-agent") || "",
});functions/index.js. Sigue en el repositorio, pero nada del sitio la invoca.El segundo intento fue una ruta API de Next.js (/api/send-email) usando Resend, con plantillas HTML bilingües completas. Técnicamente más prolija que la primera, pero tiene un problema de fondo: es una ruta dinámica, y Next.js no incluye rutas que dependan de la request cuando el sitio se exporta como estático. El build la marca como ruta de servidor (ƒ), y esa marca es, en los hechos, una sentencia de muerte para un sitio en Firebase Hosting.
El tercer intento, el que está en producción, manda el formulario directamente desde el navegador con EmailJS, en dos llamadas a dos plantillas distintas.
await emailjs.send(SERVICE_ID, 'template_9mu4p5c', {
nombre, email, telefono, asunto, mensaje,
cc_email: 'lexxionaabogados@gmail.com',
}, PUBLIC_KEY);
await emailjs.send(SERVICE_ID, 'template_zvnj7ov', {
nombre, email, telefono, asunto, mensaje,
}, PUBLIC_KEY);
window.location.href = '/gracias';src/app/contacto/page.js. Esto es lo que corre en producción hoy.Ninguna de las tres decisiones fue arbitraria en su momento. Cada una resolvía algo que la anterior no resolvía bien, hasta llegar a una que corre de forma confiable dentro de las restricciones reales del export estático.
Cómo validé esta arquitectura
Corrí npm run build contra el repositorio real (Next.js 16.1.6, Turbopack) y confirmé el resultado en vez de describirlo de memoria.
const nextConfig = {
output: 'export',
trailingSlash: true,
images: { unoptimized: true },
};next.config.mjs.Resultados medibles
| Métrica | Resultado | Entorno / evidencia |
|---|---|---|
| Lighthouse Performance (móvil, lab) | 71 / 100 | Lighthouse 13.5.0, throttling simulado |
| LCP (lab) | 6,9 s | Misma medición Lighthouse, no es un dato de campo (CrUX) |
| Tiempo de build local | 7,26 s | next build con Turbopack, Next.js 16.1.6 |
| Tamaño del export estático | 11 MB en 720 archivos | Carpeta /out generada por ese build |
| Rutas generadas | 79 (77 estáticas, 2 SSG) | Log del build. La ruta dinámica de Resend no se incluye |
| Commits de desarrollo | 91, de julio a octubre de 2026 | git log del repositorio |
SEO y GEO: lo que dice el robots.txt y lo que realmente controla
El robots.txt real del sitio habilita explícitamente a GPTBot, ClaudeBot, PerplexityBot, Google-Extended y otros, y bloquea scrapers SEO agresivos. El propio archivo etiqueta a Google-Extended como «Google Gemini & AI Overviews», pero según la documentación de Google ese token no controla la inclusión en Google Search ni en AI Overviews: solo decide si el contenido puede usarse para entrenar modelos y fundamentar respuestas. Vale la pena no repetir esa asociación como si fuera exacta.
La arquitectura correcta no es la que se diseña bien en el papel, es la que sigue funcionando después del segundo intento fallido.
Mi participación en este proyecto
Mi participación en Lexiona Abogados incluyó el desarrollo web completo, las campañas de Google y Meta Ads, y la integración de tracking de conversiones, como parte de mi trabajo en MANYA Digital.
Lo que haría distinto: código muerto que debería dejar de estarlo
La Cloud Function con Nodemailer y la ruta con Resend no rompen nada hoy porque nadie las llama. Pero quedan ahí, como dos caminos que alguien podría tomar por error pensando que están activos. No hay ninguna nota en el repositorio que diga cuál de los tres mecanismos es el real.
Tampoco verifiqué si el formulario en producción tiene alguna protección contra spam. EmailJS no lo resuelve solo, y es el tipo de pregunta que vale la pena responder antes de que se convierta en un problema.
Qué probaría después
Documentaría en el propio repositorio cuál de los tres mecanismos de envío está activo, para que nadie pierda tiempo con los otros dos, y evaluaría si conviene borrar el código muerto directamente en vez de dejarlo como posibilidad confusa. También revisaría si el formulario necesita rate limiting o un captcha, algo que no llegué a verificar en esta entrega.
Fuentes
Preguntas frecuentes
¿Por qué el sitio terminó enviando el formulario con EmailJS en vez de un backend propio?
Porque el sitio es un export estático de Next.js y no puede ejecutar rutas de servidor en producción. Antes de llegar a EmailJS probé una Cloud Function con Nodemailer y una ruta API con Resend; ninguna de las dos llegó a producción tal como estaban planteadas.
¿Qué pasó con la Cloud Function y la ruta con Resend?
Las dos siguen en el repositorio, pero no se usan. La Cloud Function escribe leads en Firestore y nunca se llama desde el sitio. La ruta con Resend es una API route dinámica, y Next.js no la incluye en un export estático: no puede ejecutarse en producción tal como está.
¿Es seguro enviar el email directamente desde el navegador con EmailJS?
La clave pública de EmailJS está pensada para exponerse en el cliente, como una API key pública. El riesgo real es el abuso del formulario (spam), no la filtración de credenciales. No verifiqué si el proyecto tiene rate limiting o captcha en esta entrega.
¿Cómo mide el despacho si las campañas funcionan, si no hay un CRM?
Con datos de las propias plataformas de ads: la Conversions API de Meta recibe eventos de conversión desde una Cloud Function, y PostHog registra comportamiento en el sitio. Ninguno de los dos reemplaza un CRM con seguimiento de casos.
¿Por qué 37 landings en vez de una sola página de servicios?
Porque cada tipo de accidente (choque, atropello, siniestro con fuga, 19 tipos de indemnización, entre otros) es una búsqueda distinta con intención distinta. Una sola página genérica compite peor por cada término específico que una landing dedicada.
¿Cuánto cuesta mantener un sitio así?
El hosting es mínimo porque es un export estático en Firebase Hosting. El costo real está en las campañas de Google y Meta Ads, y en el tiempo de mantener contenido (37 landings, 6 artículos de blog) actualizado.