Ingeniero SRE frente a sus monitores, representando la cultura DevOps

Construyendo Se2Code: De Cero a Blog Técnico con Astro, Docker y Directus

Proyectos

Introducción: ¿Por qué construir desde cero?

En un mundo donde existen decenas de plataformas de blogging listas para usar (Medium, Dev.to, Ghost, WordPress), ¿por qué invertir tiempo en construir tu propio blog?

La respuesta es simple: el viaje es tan importante como el destino.

Como ingenieros de software, a veces caemos en la trampa de usar herramientas sin entender cómo funcionan por dentro. Este blog nace con dos propósitos:

  1. Aprender haciendo: Dominar tecnologías modernas como Astro, Docker y Directus
  2. Documentar el proceso: Compartir los desafíos técnicos y las soluciones encontradas

En este artículo, te cuento exactamente cómo construí Se2Code, las decisiones arquitectónicas que tomé y los problemas que resolví en el camino.

🏗️ La Arquitectura: Un Stack Moderno y Minimalista

El Diagrama Mental

┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Astro │────▶│ Directus │────▶│ PostgreSQL │
│ (Frontend) │ │ (Backend) │ │ (Database) │
└─────────────┘ └──────────────┘ └─────────────┘


┌─────────────┐
│ Docker │
│ (Orquestador│
└─────────────┘

¿Por qué estas tecnologías?

Astro:

  • Rendimiento: Genera HTML estático por defecto, con hidratación parcial solo cuando es necesario
  • Flexibilidad: Permite usar componentes de React, Vue o Svelte si los necesitas
  • Developer Experience: Sintaxis familiar basada en HTML + JavaScript

Directus:

  • Headless CMS: Separa el contenido de la presentación
  • API Automática: Genera endpoints REST y GraphQL instantáneamente
  • Panel de Administración: Interfaz intuitiva para gestionar contenido sin tocar código

Docker:

  • Consistencia: El mismo entorno en desarrollo y producción
  • Aislamiento: Cada servicio (Astro, Directus, PostgreSQL) corre en su propio contenedor
  • Escalabilidad: Fácil de migrar a Kubernetes cuando sea necesario

El Diseño: Identidad DevOps

Quería que el sitio transmitiera profesionalismo técnico sin ser aburrido. El resultado: un tema oscuro inspirado en terminales y editores de código.

Paleta de Colores

css
--primary: #00ff88; /* Verde neón tipo terminal /
--secondary: #61dafb; /
Azul React/Astro /
--bg: #0a0a0a; /
Negro casi puro /
--bg-card: #1f1f23; /
Gris oscuro para tarjetas /
--text: #e4e4e7; /
Blanco suave */

Elementos Clave

Favicon Animado: Un SVG que simula una terminal con cursor parpadeante
Header Terminal: En móvil, un menú que se despliega como una ventana de terminal real (con los 3 puntos de colores de macOS)
Footer Consistente: Blindado contra conflictos de CSS para que se vea idéntico en todas las páginas

🚧 Los Desafíos Técnicos (y Cómo los Resolví)

  1. El Footer que No Se Veía Igual en Todas Partes
    Problema: El footer se veía perfecto en el home, pero en los artículos aparecía sin márgenes y con una fuente diferente.
    Diagnóstico:
  • Las variables CSS (--bg-alt, --primary) no estaban disponibles en la página del artículo
  • La clase .container del artículo (con max-width: 800px) estaba "secuestrando" al footer
    Solución:
  • Crear un Layout.astro global que importe global.css una sola vez
  • Usar process.env en lugar de import.meta.env para variables de entorno en SSR
  • Renombrar .container a .article-container en los artículos para evitar conflictos
// Antes (no funcionaba)
const smtpHost = import.meta.env.SMTP_HOST;
// Después (funciona)
const smtpHost = process.env.SMTP_HOST;
  1. El Formulario de Contacto que No Enviaba Emails
    Problema: El formulario guardaba los datos en Directus, pero los emails nunca llegaban.
    Causa Raíz:
  • Astro en modo SSR (Server-Side Rendering) no puede acceder a import.meta.env en tiempo de ejecución.

  • Las variables de entorno solo están disponibles en build time.
    Solución:

  • Usar process.env para leer variables en runtime

  • Configurar Nodemailer con SMTP (sin depender de servicios de terceros)

  • Escapar caracteres peligrosos en el mensaje para evitar inyección HTML

const safeMessage = message
  .replace(/"/g, '"')
  .replace(/'/g, ''')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;');
  1. URLs Sucias que Afectaban el SEO
    Problema: El menú de terminal (diseñado para móvil) se mostraba también en pantallas grandes.
    Solución:
  • Aplicar display: none por defecto al menú móvil
  • Usar media queries precisas (@media (max-width: 768px))
  • Implementar visibility: hidden + opacity: 0 para transiciones suaves
.terminal-menu {
  display: none; /* Oculto por defecto */
}

@media (max-width: 768px) {
  .terminal-toggle {
    display: block; /* Mostrar botón solo en móvil */
  }
  
  .terminal-menu.active {
    display: flex; /* Mostrar solo cuando está activo */
  }
}

🔐 Seguridad y SEO
Protección Contra Spam
Implementé Cloudflare Turnstile (una alternativa gratuita a reCAPTCHA) que:

  • Verifica que el usuario es humano sin interacción visible
  • No requiere que el usuario resuelva acertijos de "selecciona los semáforos"
  • Es gratuito hasta 100,000 solicitudes/mes

Control de Indexación
Agregué una propiedad noindex al Layout para controlar qué páginas ve Google:

<Layout title="Contacto" noindex={true}>
  <!-- Esta página no aparecerá en Google -->
</Layout>

Esto es crucial para páginas como /contact que no deben indexarse.

📧 Emails con Diseño Profesional

Los emails no son solo texto plano. Usé tablas HTML con estilos inline (la única forma confiable de hacer emails que se vean bien en clientes como Gmail, Outlook, etc.):

  • Fondo oscuro: #0a0a0a
  • Bordes: Verdes neón
  • Tipografía: Monoespaciada
  • Efecto visual: "Terminal de transmisión entrante"

📊 Resultados y Métricas

Lo que Logramos

  • Performance: Lighthouse score de 95+ en performance y accesibilidad
  • SEO: URLs limpias, meta tags dinámicos y sitemap automático
  • UX: Formulario funcional con validación en tiempo real
  • Mantenibilidad: Código modular, componentes reutilizables
  • Escalabilidad: Arquitectura lista para migrar a Kubernetes

💡 Lecciones Aprendidas

  1. Las variables de entorno en SSR son diferentes: Entender la diferencia entre process.env vs import.meta.env es vital.
  2. Los emails son un infierno: Siempre usa tablas y estilos inline para máxima compatibilidad.
  3. Docker Compose es tu amigo: Pero el flag --force-recreate es tu mejor amigo.
  4. El naming importa: Un simple nombre vs name puede romper toda tu API.

🚀 Próximos Pasos

Este blog es un proyecto vivo. Actualmente en desarrollo:

  • Migración a Kubernetes: Traducir el docker-compose.yml a manifiestos de K8s.
  • Búsqueda Full-Text: Implementar búsqueda de artículos con PostgreSQL.
  • Analytics Self-Hosted: Usar Plausible o Umami en lugar de Google Analytics para cuidar la privacidad.
  • CI/CD: Configurar GitHub Actions para deployments automáticos.

🏁 Conclusión

Construir este blog desde cero no fue solo sobre escribir código; fue sobre tomar decisiones arquitectónicas, resolver problemas complejos y documentar el proceso para que otros puedan aprender.

Si llegaste hasta aquí, ¡gracias por leer! Si tienes preguntas o sugerencias, usa el formulario de contacto —¡te aseguro que realmente funciona!

¿Qué construirás tú hoy?