Portfolio personal de Gustavo Baranda, Full Stack Developer. Construido con Next.js 16 App Router, React 19 y TypeScript para reflejar criterio de producción real: protección anti-spam sin CAPTCHA, SEO técnico avanzado, sistema de temas sin hydration flash y layout con CSS subgrid moderno.
Esta sección resume las implementaciones con mayor criterio de producción — pensadas para entrevistas técnicas y revisiones de código.
El formulario de contacto combina tres capas independientes sin degradar la experiencia del usuario:
| Capa | Implementación | Archivo |
|---|---|---|
| Honeypot field | Campo oculto (display:none + tabIndex={-1} + autoComplete="off") invisible para humanos pero completado por bots. El backend rechaza silenciosamente con 200 OK para no alertar al bot. |
ContactForm.tsx / route.ts |
| Time-based validation | Timestamp de inicio del formulario incluido en el payload. Envíos en < 2 s se rechazan silenciosamente. | ContactForm.tsx / route.ts |
| Rate limiting in-memory | Ventana deslizante de 3 requests / IP cada 10 min. Implementado sin dependencias externas, con comentario explícito sobre el comportamiento en cold starts de Vercel. | src/app/api/contact/route.ts |
El rechazo silencioso con
200 OKen honeypot y time-based es intencional: evita que bots adaptativos detecten que están siendo filtrados.
// layout.tsx — ejecutado ANTES de que React hidrate
<Script id="theme-toggle" strategy="beforeInteractive" dangerouslySetInnerHTML={{ __html: themeScript }} />El script lee localStorage sincrónicamente y aplica la clase dark al <html> antes del primer paint. Los tokens CSS semánticos (--canvas, --surface, --text, --muted, --accent) se intercambian a nivel de :root / :root.dark, sin ninguna variable de estado de React ni useEffect que cause parpadeo.
Las tarjetas de proyectos usan grid-rows-subgrid de CSS Grid Level 2 para alinear secciones homólogas (contenido / stack técnico / links) entre columnas sin JavaScript:
/* ProjectsGrid — contenedor con 3 filas implícitas por fila de cards */
grid-template-rows: auto auto auto;
/* ProjectCard — hereda las filas del padre */
grid-row: span 3;
display: grid;
grid-template-rows: subgrid;Cuando una tarjeta expande su sección de solución, la fila del grid se estira y las tarjetas vecinas alinean automáticamente su STACK TÉCNICO y sus links al mismo nivel sin ningún cálculo de altura en JS.
metadataBasedinámico: resuelve todas las URLs de OpenGraph, Twitter Cards y canonical contra la URL base correcta según entorno.getSiteUrl()con prioridad de entorno:NEXT_PUBLIC_BASE_URL→VERCEL_URL(preview deployments automáticos) → fallback hardcoded. Fuerzahttps://en producción aunque la variable tengahttp://.- Sitemap y robots dinámicos generados por Next.js en build time (
src/app/sitemap.ts,src/app/robots.ts). - JSON-LD
PersonconsameAsGitHub/LinkedIn,knowsAboutyalumniOfpara rich results de Google.
| Tecnología | Versión | Motivo |
|---|---|---|
| Next.js App Router | 16 | Server Components por defecto, rutas de API integradas, generación estática de sitemap/robots |
| React | 19 | Concurrent features, useOptimistic disponible para futuras mejoras del formulario |
| TypeScript | 6 | Tipado estricto en componentes, datos y respuestas de API |
| Tailwind CSS | v4 | Design tokens nativos, subgrid support, @custom-variant para dark mode sin config extra |
| Framer Motion | 12 | Animaciones declarativas con AnimatePresence para transiciones de estado del formulario |
| Tecnología | Motivo |
|---|---|
| Next.js API Routes | Serverless functions en el mismo repo, sin servidor externo |
| Nodemailer + Gmail SMTP | Envío de email con plantilla HTML personalizada y adjunto de logo inline (CID) |
| Tecnología | Motivo |
|---|---|
| Vercel | Deploy automático desde GitHub, edge network, variables de entorno por entorno |
| ESLint + eslint-config-next | Reglas de Next.js + React Hooks linting |
GusDev/
├── public/ # Imágenes estáticas, OG image, logos de proyectos
└── src/
├── app/ # Next.js App Router — páginas y rutas
│ ├── api/contact/ # POST endpoint — validación, honeypot, rate limit, Nodemailer
│ ├── about/ # Página About (Server Component)
│ ├── contact/ # Página Contact
│ ├── projects/ # Página Proyectos
│ ├── layout.tsx # Root layout — metadataBase, JSON-LD, skip-to-content, tema
│ ├── page.tsx # Hero page
│ ├── sitemap.ts # Sitemap dinámico (build time)
│ └── robots.ts # robots.txt dinámico
├── components/
│ ├── contact/ # ContactForm.tsx (honeypot, time validation), ContactInfo.tsx
│ ├── header/ # Header sticky, menú mobile con drawer, ThemeToggle
│ ├── hero/ # TiltImage, FillButton, TransitionEffect
│ ├── projects/ # ProjectsGrid.tsx (filtros), ProjectCard.tsx (subgrid)
│ ├── about/ # HeroBio, ExperienceSection, EducationSection
│ ├── common/ # SocialLinks
│ ├── utils/ # AnimatedText
│ └── footer.tsx
├── config/
│ └── site.ts # getSiteUrl() con lógica de entorno y fallbacks
├── data/
│ └── projectsData.ts # Datos tipados de proyectos y categorías
└── types/
└── index.ts # Interfaces TypeScript globales (ContactFormData, ApiResponse)
- Node.js
>= 20.x - npm
>= 10.x(o pnpm / yarn equivalente) - Cuenta de Gmail con Contraseña de Aplicación habilitada (requiere 2FA activo)
# 1. Clonar el repositorio
git clone https://github.com/GustavoBaranda/Portfolio.git
cd Portfolio
# 2. Instalar dependencias
npm install
# 3. Configurar variables de entorno
cp .env.example .env.local
# → Editar .env.local con tus valores (ver sección siguiente)
# 4. Iniciar servidor de desarrollo
npm run dev
# → http://localhost:3000Crear .env.local basándose en el siguiente template:
# ── Email (Formulario de Contacto) ────────────────────────────────────────────
# Cuenta Gmail desde la que se envían los emails
EMAIL_USER=tu_correo@gmail.com
# Contraseña de Aplicación de Google (NO tu contraseña de cuenta)
# Generarla en: myaccount.google.com/apppasswords
# Formato con espacios es válido: "abcd efgh ijkl mnop"
EMAIL_PASS=tu_contrasena_de_aplicacion
# ── SEO / URL Base ─────────────────────────────────────────────────────────────
# En local: http://localhost:3000
# En producción (Vercel): https://tudominio.com
# Si no se define, getSiteUrl() cae al fallback https://gustavobaranda.com
NEXT_PUBLIC_BASE_URL=http://localhost:3000En Vercel: Configurar
NEXT_PUBLIC_BASE_URL=https://tudominio.comúnicamente en el entorno Production. Los preview deployments usaránVERCEL_URLautomáticamente.
npm run dev # Servidor de desarrollo con HMR
npm run build # Build de producción (type-check + export estático)
npm run start # Servidor de producción local (requiere build previo)
npm run lint # ESLint con reglas de Next.js# Verificar sitemap (PowerShell)
(Invoke-WebRequest -Uri "https://gustavobaranda.com/sitemap.xml" -UseBasicParsing).Content
# Verificar robots.txt
(Invoke-WebRequest -Uri "https://gustavobaranda.com/robots.txt" -UseBasicParsing).Content
# Verificar canonical y OG tags
(Invoke-WebRequest -Uri "https://gustavobaranda.com" -UseBasicParsing).Content | Select-String "canonical|og:url|og:image"Herramientas externas:
- OpenGraph preview → opengraph.xyz
- Rich Results → search.google.com/test/rich-results
- Page speed → pagespeed.web.dev
MIT — ver LICENSE.