Skip to content
SoyGarfield
Research #04

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
Línea de tiempo con tres intentos de enviar el formulario de contacto. Intento 1: Cloud Function más Nodemailer, construida pero nunca llamada desde el sitio. Intento 2: ruta API más Resend, no puede correr en un export estático. Intento 3, en producción: EmailJS del lado del cliente.
Dos intentos abandonados y uno en producción. Los tres siguen visibles en el código y en el historial de git. Diagrama propio.

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") || "",
});
Código real (trimmed) de la primera Cloud Function, en 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';
Código real de 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 },
};
Código real de next.config.mjs.

Resultados medibles

Métricas reales medidas el 18 de octubre de 2026.
MétricaResultadoEntorno / evidencia
Lighthouse Performance (móvil, lab)71 / 100Lighthouse 13.5.0, throttling simulado
LCP (lab)6,9 sMisma medición Lighthouse, no es un dato de campo (CrUX)
Tiempo de build local7,26 snext build con Turbopack, Next.js 16.1.6
Tamaño del export estático11 MB en 720 archivosCarpeta /out generada por ese build
Rutas generadas79 (77 estáticas, 2 SSG)Log del build. La ruta dinámica de Resend no se incluye
Commits de desarrollo91, de julio a octubre de 2026git 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.

Pietro Fiorillo

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.