Portafolio personal
Una cartera moderna y centrada en el contenido construida con Next.js 16, React 19 y TypeScript. Construida desde cero con desarrollo asistido por IA.
Durante años me dije que construiría un sitio personal. El dominio acumulaba polvo. La idea nunca salió de la aplicación de notas.
Entonces dejé de planearlo y empecé a construirlo. Dos días más tarde, este sitio estaba vivo.
Qué cambió
Yo solía pensar que el envío de un sitio significaba semanas de trabajo. Selección de fuentes. Depurar diseños. Escribir textos que nunca me parecían correctos. Esta vez tenía un enfoque diferente: construir con IA, enviar rápido, mejorar más tarde.
El resultado: 51 commits en 48 horas. Un blog con narración de audio. Un formulario de contacto que realmente funciona. Un sistema de diseño que puedo ampliar. No es perfecto, pero es real.
Lo que me sorprendió no fue la velocidad. Era cómo se sentía el trabajo. Menos búsqueda, más construcción. Menos dudas, más envío. Las herramientas se encargaban de las partes tediosas para que yo pudiera centrarme en lo importante.
Esta página es la historia completa. Cómo se hizo, qué hay bajo el capó y qué haría de forma diferente la próxima vez.

Tech Stack
| Categoría | Tecnología | Versión | Propósito |
|---|---|---|---|
| Framework | Next.js (App Router) | 16.0.10 | Componentes de servidor, enrutamiento, SSR |
| Biblioteca UI | React | 19.2.3 | Arquitectura de componentes |
| Lenguaje | TypeScript | 5.x | Seguridad de tipos |
| Estilos | Tailwind CSS | 4.x | Estilos utility-first |
| Componentes | shadcn/ui | New York | Componentes accesibles pre-construidos |
| Contenido | next-mdx-remote | 5.0.0 | Renderizado MDX con frontmatter |
| Parsing | gray-matter | 4.0.3 | Extracción de frontmatter |
| Iconos | Lucide React | 0.561.0 | Biblioteca de iconos SVG |
| SendGrid | 8.1.6 | Envío de formularios de contacto | |
| Analytics | Google Analytics 4 | — | Seguimiento de tráfico y comportamiento |
| Audio | ElevenLabs Audio Native | — | Texto a voz para entradas de blog |

Arquitectura
La aplicación sigue las convenciones de Next.js App Router con una clara separación entre páginas, componentes, contenido y utilidades.
ftchvs_26/
├── app/ # Páginas de Next.js App Router
│ ├── layout.tsx # Diseño raíz con fuentes y nav
│ ├── page.tsx # Página de inicio
│ ├── globals.css # Estilos globales y tokens de diseño
│ ├── about/ # Acerca de la página
│ ├── blog/ # Páginas de listado y [slug] del blog
│ ├── proyectos/ # Listado de proyectos y páginas [slug]
│ ├── contacto/ # Formulario de contacto con ruta API
│ └── curriculum vitae/ # Página de curriculum vitae
├── components/ # Componentes React
│ ├── nav.tsx # Navegación con hoja móvil
│ ├── theme-toggle.tsx # Cambio de modo oscuro/claro
│ ├── tts-player.tsx # Reproductor de audio ElevenLabs
│ ├── mdx-content.tsx # Renderizador MDX
│ └── ui/ # componentes shadcn/ui
├── content/ # archivos de contenido MDX
│ ├── blog/ # entradas de blog
│ ├── pages/ # Páginas estáticas (acerca de)
│ └── proyectos/ # Casos prácticos de proyectos
├── lib/ # Utilidades
│ ├── content.ts # Carga y análisis sintáctico del contenido
│ ├── constants.ts # Configuración del sitio
│ ├── schema.tsx # Datos estructurados en JSON-LD
│ └── utils.ts # Funciones de ayuda
└── public/ # Activos estáticos
Flujo de datos
El contenido fluye desde los archivos MDX a través de lib/content.ts a las páginas:
- Los archivos MDX almacenan el contenido con el frontmatter YAML
-
- gray-matter analiza el frontmatter en objetos tipados
- next-mdx-remote serializa MDX para la renderización React
- Page components obtiene el contenido en el momento de la petición
- Componentes MDX renderizan el HTML final

Sistema de diseño tipográfico
Construí un sistema de tipografía basado en tokens para un estilo consistente en todas las páginas. Cada token se asigna a clases específicas de Tailwind.
| Token | Clase CSS | Estilos | Uso |
|---|---|---|---|
| Título de página | .text-page-title | text-xl font-medium mb-6 | Encabezados H1 |
| Título de sección | .text-section-title | text-lg font-medium mt-8 mb-4 | Encabezados H2 |
| Subsección | .text-subsection-title | text-base font-medium mt-6 mb-3 | Encabezados H3 |
| Cuerpo | .text-body | text-[15px] leading-relaxed text-foreground/90 | Párrafos |
| Contenedor de cuerpo | .text-body-container | space-y-4 text-[15px] leading-relaxed | Múltiples párrafos |
| Meta | .text-meta | text-[14px] text-muted-foreground | Fechas, etiquetas, rótulos |
| Enlace | .text-link | underline underline-offset-2 hover:text-foreground | Enlaces en línea |
| Elemento de lista | .text-list-item | text-sm text-foreground/80 | Texto de tarjetas y listas |
Pila de fuentes
- Geist Sans — Tipo de letra principal para el cuerpo del texto y la interfaz de usuario
- Geist Mono — Bloques de código y logotipo del terminal
Ambas fuentes se cargan a través de next/font/google con display: swap para un rendimiento óptimo.
Sistema de colores
Los colores utilizan el espacio de color OKLCH para ajustes perceptualmente uniformes. El sistema incluye:
- Modo claro - Escala de grises neutra con acentos sutiles.
- Modo oscuro** - Fondos profundos con texto de alto contraste
- Colores semánticos - Primarios, secundarios, apagados, de acento, destructivos, de éxito
Características principales
Modo Oscuro
La preferencia de tema persiste en localStorage y respeta la configuración del sistema en la primera visita. Un script de bloqueo en <head> previene el flasheo de un tema incorrecto.
<script peligrosamenteSetInnerHTML={{
__html: `(function(){
var t = localStorage.getItem('theme') ||
(matchMedia('(prefiere-esquema-de-color:oscuro)').matches ? 'oscuro' : 'claro');
if (t === 'dark') document.documentElement.classList.add('dark')
})();`,
}} />
Text-to-Speech
El reproductor se carga dinámicamente para evitar problemas de hidratación:
const TTSPlayer = dynamic(
() => import('@/components/tts-player').then((mod) => mod.TTSPlayer),
{ ssr: false }
);
Crear un clon de voz en ElevenLabs
Para crear tu propio clon de voz para el reproductor de audio:
- Regístrate o inicia sesión en ElevenLabs
- Navega hasta la sección Voces en el dashboard (barra lateral izquierda)
- Haz clic en Añadir una nueva voz
- Elige Clon de voz profesional (recomendado para obtener la mejor calidad) o Clon de voz instantáneo (más rápido, requiere menos audio)
-
- Sube muestras de audio:
- Clon de voz profesional: Al menos 1 hora de audio de alta calidad (idealmente 2-3 horas para obtener mejores resultados)
- Clon de voz instantáneo**: Mínimo 30 segundos de audio limpio
- Divida las grabaciones largas en muestras de unos 30 minutos para facilitar la carga.
- Utilice grabaciones limpias y de alta calidad sin ruido de fondo, eco o sonidos no deseados
- Opcionalmente, grabe directamente en la interfaz utilizando Grabe usted mismo con los guiones de muestra proporcionados
- Nombra y etiqueta tu clon de voz
- Confirme que tiene el derecho y el consentimiento para clonar la voz
- Haga clic en Guardar voz para entrenar el modelo
- Una vez entrenado, copie el ID de voz de los ajustes de voz
- Configurarlo en el proyecto actualizando
ELEVENLABS_VOICE_IDenlib/constants.ts
Ejemplo de voz
Aquí tienes una muestra de la voz clonada:
Para generar la muestra de audio, puedes utilizar el ElevenLabs Text to Speech playground. Selecciona tu voz clonada, introduce el texto, haz clic en Generar y, a continuación, descarga y guarda el archivo como voice-sample.mp3 en el directorio public/audio/.
Narración de audio del blog (Kokoro local)
Actualización — abr/2026. Las publicaciones del blog y la página About ahora usan Kokoro-82M, un modelo TTS con licencia Apache 2.0 (82M parámetros, voz af_heart) ejecutándose localmente en CPU mediante kokoro-onnx. La generación es determinista, gratis y no depende de ninguna API externa. La muestra de voz anterior se mantiene como la voz clonada original de ElevenLabs — solo se migró la narración del blog.
La configuración es por única vez: npm run setup:kokoro crea un directorio .kokoro/ ignorado por git con un venv de Python y el modelo ONNX (~340 MB). Después, npm run generate-audio narra todo el contenido en inglés y actualiza public/audio/manifest.json con hashes por post. En un Apple M4 Max el modelo corre a ~5× tiempo real — un post de tres minutos tarda unos treinta segundos.
Formulario de contacto
El formulario de contacto utiliza SendGrid para la entrega de correo electrónico con:
- Validación del lado del cliente
- Ruta API del lado del servidor en
/contact/api. - Encabezado Reply-to para respuestas directas
- Gestión de errores con mensajes fáciles de usar
SEO
Cada página incluye:
- Metadatos** - Título, descripción, palabras clave
- Open Graph** - Imágenes y texto para compartir en redes sociales
- Twitter Cards** - Formato específico de Twitter
- JSON-LD**: datos estructurados para persona, sitio web, artículo, rastreo.
- Mapa del sitio** - Autogenerado en
/sitemap.xml. - Robots** - Reglas de rastreo de los motores de búsqueda

Build Process
Fase 1: Configuración del proyecto
Comenzamos con create-next-app usando la plantilla App Router. Añadimos TypeScript, Tailwind CSS 4, y configuramos shadcn/ui con la variante de estilo New York.
``bash npx create-next-app@latest ftchvs_26 --typescript --tailwind --app npx shadcn@latest init
### Fase 2: Diseño del núcleo
Construimos el diseño raíz con fuentes Geist y una navegación responsiva. La navegación incluye un menú de escritorio y una hoja móvil (cajón deslizante) de shadcn/ui.
El logotipo del terminal (`>_`) utiliza una animación que se adapta a los modos claro y oscuro.
### Fase 3: Sistema de contenidos
Creado `lib/content.ts` para manejar la carga MDX:
- `getAllPosts()` - Obtener todas las entradas del blog ordenadas por fecha
- GetPostBySlug()` - Obtiene una entrada individual con MDX serializado
- getAllProjects()` - Obtiene todos los proyectos
- `getProjectBySlug()` - Obtener un único proyecto con MDX serializado
- `getPageBySlug()` - Obtener páginas estáticas como Acerca de
Cada función lee del sistema de archivos, analiza el frontmatter con gray-matter, y serializa el contenido con next-mdx-remote.
### Fase 4: Páginas
Construimos seis rutas principales:
- **Inicio** - Introducción con lista de escritos recientes
- Acerca de** - Historial profesional cargado desde MDX
- **Blog** - Listado de entradas y páginas de entradas individuales
- **Proyectos** - Escaparate de proyectos (esta página)
- CV** - Experiencia profesional y habilidades
- Contacto** - Formulario integrado con SendGrid
### Fase 5: Integraciones
**SendGrid**: Configurada la ruta API para enviar emails desde el formulario de contacto. El remitente debe estar verificado en la configuración de autenticación de SendGrid.
**Google Analytics 4**: Añadida vía `@next/third-parties` con carga condicional basada en `NEXT_PUBLIC_GA_ID`.
### Fase 6: Sistema de diseño
Implementado tokens de diseño de tipografía en `globals.css`. Añadidas variables de color OKLCH para los modos claro y oscuro. Creada animación shimmer con propiedades CSS personalizadas.
### Fase 7: SEO
Añadidos metadatos a todas las páginas. Creados generadores de sitemap y robots.txt. Implementación de datos estructurados JSON-LD para los esquemas Persona, WebSite, Artículo y BreadcrumbList.
### Fase 8: Despliegue
Empujado a GitHub e importado a Vercel. Configuradas las variables de entorno:
- `NEXT_PUBLIC_SITE_URL` - URL canónica
- `NEXT_PUBLIC_GA_ID` - Google Analytics
- `SENDGRID_API_KEY` - Envío de correo electrónico
- `CONTACT_EMAIL_TO` - Dirección del destinatario
- `CONTACT_EMAIL_FROM` - Remitente verificado
{/* TODO: Añadir captura de pantalla - Vercel deployment */}
<figure>
<img src="/images/projects/portfolio/vercel-deployment.png" alt="Vercel Deployment" />
<figcaption>Detalles del despliegue Vercel</figcaption>
</figure>
---
## Changelog
Un cronograma detallado del desarrollo, extraído del historial de commits de Git. El último primero.
### Estadísticas de desarrollo
- Total de commits**: 51
- Tiempo de desarrollo ~2 días
- Principales características**: 8
- Corrección de errores**: 12
- Refactorizaciones**: 15
- Actualizaciones de estilo**: 16
---
### 13 de diciembre de 2025 - Día 2: Pulido y Características
#### Noche: Pulido final
| Compromiso | Descripción |
|--------|-------------|
| `20107f4` | **Major**: Mejoras integrales de SEO
| Simplificación de la introducción de la página "Acerca de".
| Configuración centralizada de ID de voz.
| Configuración de voz ElevenLabs personalizada
| Importante: Optimizado ElevenLabs para reducir costes de API.
| Posicionado el reproductor TTS sobre el resumen.
| Actualizada la tipografía de la lista del blog.
| Revisada la entrada del blog para mayor claridad y narrativa.
#### Tarde: Optimización de audio
| Compromiso Descripción
|--------|-------------|
| Usado ref para insertar HTML (arreglo de hidratación).
| Usar incrustación iframe para audio nativo.
| Restaurada la envoltura de Suspense para compatibilidad con MDX.
| Importante: API de Audio Native con proyectos de contenido completo.
| Eliminado Suspense para SSR'd Audio Native.
| Cargar Audio Native mediante useEffect
| Codificado el ID de usuario público.
| Usar siguiente/script para cargar Audio Native
| Simplificado a la integración de Audio Native
| Añadido script para borrar la caché de audio.
| Añadido caché de audio para reducir las llamadas a la API.
| Generación previa de audio TTS al cargar la página.
| Añadido registro de depuración al reproductor TTS.
#### Tarde: Sprint de integración de audio
| Compromiso Descripción
|--------|-------------|
| Eliminado el pie de ElevenLabs del reproductor.
| Hecho el selector de velocidad más minimalista.
| Eliminado el encabezado de ElevenLabs del reproductor
| Mostrar inmediatamente toda la interfaz de usuario del reproductor de audio
| Mejorado el reproductor TTS con componentes de interfaz de ElevenLabs
| Reemplazar Audio Native con TTS API
| Añadida configuración y reglas de Cursor IDE
| Añadido MDXRemote con Suspense para React 19
| Mayor: Añadida integración ElevenLabs Audio Native TTS.
#### Tarde: Tipografía y Curriculum Vitae
| Descripción
|--------|-------------|
| Actualizado el enunciado de aprendizaje en la entrada del blog.
| Actualizado el eslogan del currículum.
| Añadido el prefijo guión a los elementos de habilidades y educación.
| Eliminada la sección "Buscando" del currículum.
| Mejorada la tipografía y alineación del currículum.
| Actualizado el tamaño de letra de los elementos de la lista, enlaces y meta texto.
#### Tarde por la mañana: Estrategia de contenidos
| Compromiso Descripción
|--------|-------------|
| Añadido objetivo de audiencia a la página "Acerca de".
| Se ha suavizado el mensaje "construyo las herramientas yo mismo".
| Actualización de README para el nuevo posicionamiento.
| Refinado el posicionamiento del vendedor de crecimiento en todas las páginas.
| Reposicionar el sitio como "Growth Marketer who codes".
| Reemplazada la página de proyectos con el marcador de posición "Próximamente".
#### Buenos días: Sistema de diseño
| Compromiso Descripción
|--------|-------------|
| Actualizado README con tokens de tipografía, SendGrid, documentos GA4.
| Reorganizado el menú de navegación: Acerca de, Blog, Proyectos, CV, Contacto.
| Importante: Diseño tipográfico, implementación de sistemas de fichas.
| Actualizado favicon a icono de terminal (`>_`)
| Eliminado el borde del anillo de enfoque en el logo del terminal.
| Refactorizada la animación de brillo para el modo claro/oscuro.
---
### 12 de diciembre de 2025 - Día 1: Fundación
| Compromiso | Descripción |
|--------|-------------|
| `0b4e0a2` | **Major**: SendGrid integrado para el formulario de contacto con una interfaz de usuario mejorada.
| Corregida la selección de texto al hacer clic en el logotipo del terminal.
| Desactivada la generación estática para MDX (corrección de compatibilidad con React 19).
| Añadidas páginas, componentes y características principales.
| Fusionada la documentación README
| Importante: Sitio inicial de Next.js con blog y estructura de proyectos.
| Inicialización de repositorio en GitHub
| Confirmación inicial de Create Next App.
**Hitos clave:**
- Estructura completa del App Router con 6 rutas
- Sistema de contenido MDX con análisis sintáctico de materia gris
- Integración de la biblioteca de componentes shadcn/ui
- Formulario de contacto con envío de correo electrónico
- Navegación responsive con cajón móvil
{/* TODO: Añadir captura de pantalla - historial de commit de GitHub */}
<figure>
<img src="/images/projects/portfolio/commit-history.png" alt="Commit History" />
<figcaption>Historial de commits de GitHub</figcaption>
</figure>
---
## Screenshots
<div className="bento-grid my-8">
<div className="bento-item bento-large overflow-hidden">
<img
src="/images/projects/portfolio/homepage-light.png"
alt="Página de inicio Light"
className="w-full h-full object-cover"
/>
<figcaption className="image-caption">Modo claro de la página de inicio</figcaption>
</div>
<div className="bento-item bento-medium bento-medium-top overflow-hidden">
<img
src="/images/projects/portfolio/homepage-dark.png"
alt="Página de inicio oscura"
className="w-full h-full object-cover"
/>
<figcaption className="image-caption">Modo oscuro de la página de inicio</figcaption>
</div>
<div className="bento-item bento-medium bento-medium-middle overflow-hidden">
<img
src="/images/projects/portfolio/mobile-nav.png"
alt="Navegación móvil"
className="w-full h-full object-cover"
/>
<figcaption className="image-caption">Navegación móvil</figcaption>
</div>
<div className="bento-item bento-small bento-small-left overflow-hidden">
<img
src="/images/projects/portfolio/contact-form.png"
alt="Formulario de contacto"
className="w-full h-full object-cover"
/>
<figcaption className="image-caption">Formulario de contacto</figcaption>
</div>
<div className="bento-item bento-small bento-small-center overflow-hidden">
<img
src="/images/projects/portfolio/blog-post.png"
alt="Blog Post"
className="w-full h-full object-cover"
/>
<figcaption className="image-caption">Entrada de blog con reproductor de audio</figcaption>
</div>
</div>
---
## What I Learned
**Cursor y Claude se encargaron de la repetición de tareas, detectaron errores y sugirieron patrones que yo no habría encontrado solo. El cuello de botella pasó de escribir a pensar.
**Los errores tipográficos detectaron errores antes de que llegaran al navegador. El tiempo de configuración inicial ahorró horas de depuración.
**Definir las clases tipográficas una vez significó un estilo consistente sin decisiones constantes. Los cambios se propagan automáticamente.
**MDX es flexible.** Los esquemas de Frontmatter permiten que el contenido dirija la interfaz de usuario. Añadir un nuevo campo lleva minutos, no refactorizaciones.
---
## Próximos pasos
- Añadir más proyectos como casos prácticos
- Implementar la búsqueda y el filtrado de entradas del blog
- Añadir un canal RSS para los suscriptores del blog
- Explorar ISR (Regeneración Estática Incremental) para la actualización de contenidos
Outcomes
- •Construido desde cero con desarrollo asistido por IA (Cursor + Claude)
- •Implantación de un sistema de fichas de diseño tipográfico personalizado
- •ElevenLabs Audio Native integrado para la conversión de texto a voz
- •Configurado SendGrid para la funcionalidad de formulario de contacto
- •Desplegado en Vercel con SEO integral